diff --git a/configure.ac b/configure.ac index 94bfaad..668857f 100644 --- a/configure.ac +++ b/configure.ac @@ -149,6 +149,60 @@ if test x$ipv6 = xtrue; then fi fi + +dnl Check for openssl +AC_MSG_CHECKING(for OpenSSL) +AC_ARG_WITH(openssl, + AC_HELP_STRING([--with-openssl@<:@=DIR@:>@],[Include openssl support (default no)]), + [WITH_OPENSSL=$withval],[WITH_OPENSSL=no]) + +OPENSSL_CFLAGS="" +OPENSSL_LIBS="" + +if test "$WITH_OPENSSL" != "no"; then + use_openssl=yes + if test "$WITH_OPENSSL" != "yes"; then + OPENSSL_CFLAGS="-I$WITH_OPENSSL/include" + OPENSSL_LIBS="-L$WITH_OPENSSL/lib" + fi +else + use_openssl=no +fi +AC_MSG_RESULT([$use_openssl]) + +AC_ARG_WITH(openssl-includes, + AC_HELP_STRING([--with-openssl-includes=DIR],[OpenSSL includes]), + [ use_openssl=yes OPENSSL_CFLAGS="-I$withval" ] +) + +AC_ARG_WITH(openssl-libs, + AC_HELP_STRING([--with-openssl-libs=DIR],[OpenSSL libraries]), + [ use_openssl=yes OPENSSL_LIBS="-L$withval" ] +) + +AC_ARG_WITH(kerberos5, + AC_HELP_STRING([--with-kerberos5],[use Kerberos5 support with OpenSSL]), + [ use_kerberos=yes ], [use_kerberos=no] +) + +if test "x$use_openssl" = "xyes"; then + if test "x$use_kerberos" != "xyes"; then + OPENSSL_CFLAGS="$OPENSSL_CFLAGS -DOPENSSL_NO_KRB5" + fi + + AC_CHECK_HEADERS([openssl/ssl.h]) + OLDLIBS="$LIBS" + AC_CHECK_LIB(crypto, BIO_f_base64, [ + AC_CHECK_LIB(ssl, SSL_new, [ OPENSSL_LIBS="$OPENSSL_LIBS -lssl -lcrypto" + AC_DEFINE(HAVE_LIBSSL, [], [Have libssl]) ], [], [ -lcrypto "$DL_LIB" ]) + ], [], []) + LIBS="$OLDLIBS" + + AC_SUBST(OPENSSL_CFLAGS) + AC_SUBST(OPENSSL_LIBS) +fi + + # check for extra compiler options (warning options) if test "${GCC}" = "yes"; then CFLAGS="${CFLAGS} -Wall -W -Wshadow -pedantic -std=gnu99" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ca5652e..9b15ab8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ ADD_DEFINITIONS(-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGE_FILES) # OPTION(WITH_OPENSSL "with openssl-support [default: on]" ON) OPTION(WITH_LUA "with lua 5.1 for lua-configfile [default: on]" ON) +OPTION(WITH_OPENSSL "with openssl-support [default: off]") OPTION(BUILD_STATIC "build a static lighttpd with all modules added") OPTION(BUILD_EXTRA_WARNINGS "extra warnings") @@ -71,17 +72,6 @@ CHECK_C_SOURCE_COMPILES(" return 0; }" HAVE_IPV6) -# IF(WITH_OPENSSL) -# CHECK_INCLUDE_FILES(openssl/ssl.h HAVE_OPENSSL_SSL_H) -# IF(HAVE_OPENSSL_SSL_H) -# CHECK_LIBRARY_EXISTS(crypto BIO_f_base64 "" HAVE_LIBCRYPTO) -# IF(HAVE_LIBCRYPTO) -# SET(OPENSSL_NO_KRB5 1) -# CHECK_LIBRARY_EXISTS(ssl SSL_new "" HAVE_LIBSSL) -# ENDIF(HAVE_LIBCRYPTO) -# ENDIF(HAVE_OPENSSL_SSL_H) -# ENDIF(WITH_OPENSSL) - # glib/gthread pkg_check_modules(GTHREAD REQUIRED gthread-2.0) pkg_check_modules(GMODULE REQUIRED gmodule-2.0) @@ -92,6 +82,17 @@ IF(WITH_LUA) SET(HAVE_LUA_H 1 "Have liblua header") ENDIF(WITH_LUA) +IF(WITH_OPENSSL) + CHECK_INCLUDE_FILES(openssl/ssl.h HAVE_OPENSSL_SSL_H) + IF(HAVE_OPENSSL_SSL_H) + CHECK_LIBRARY_EXISTS(crypto BIO_f_base64 "" HAVE_LIBCRYPTO) + IF(HAVE_LIBCRYPTO) + SET(OPENSSL_NO_KRB5 1) + CHECK_LIBRARY_EXISTS(ssl SSL_new "" HAVE_LIBSSL) + ENDIF(HAVE_LIBCRYPTO) + ENDIF(HAVE_OPENSSL_SSL_H) +ENDIF(WITH_OPENSSL) + IF(NOT BUILD_STATIC) CHECK_INCLUDE_FILES(dlfcn.h HAVE_DLFCN_H) ENDIF(NOT BUILD_STATIC) @@ -252,6 +253,13 @@ ADD_AND_INSTALL_LIBRARY(mod_rewrite "modules/mod_rewrite.c") ADD_AND_INSTALL_LIBRARY(mod_status "modules/mod_status.c") ADD_AND_INSTALL_LIBRARY(mod_vhost "modules/mod_vhost.c") +IF(HAVE_LIBSSL AND HAVE_LIBCRYPTO) + ADD_AND_INSTALL_LIBRARY(mod_openssl "modules/mod_openssl.c") + TARGET_LINK_LIBRARIES(mod_openssl ssl) + TARGET_LINK_LIBRARIES(mod_openssl crypto) +ENDIF(HAVE_LIBSSL AND HAVE_LIBCRYPTO) + + ADD_TARGET_PROPERTIES(common LINK_FLAGS ${COMMON_LDFLAGS}) ADD_TARGET_PROPERTIES(common COMPILE_FLAGS ${COMMON_CFLAGS}) @@ -282,11 +290,6 @@ IF(WIN32) ENDIF(MINGW) ENDIF(WIN32) -IF(HAVE_LIBSSL AND HAVE_LIBCRYPTO) - TARGET_LINK_LIBRARIES(lighttpd ssl) - TARGET_LINK_LIBRARIES(lighttpd crypto) -ENDIF(HAVE_LIBSSL AND HAVE_LIBCRYPTO) - IF(NOT WIN32) INSTALL(TARGETS ${L_INSTALL_TARGETS} RUNTIME DESTINATION ${SBINDIR} diff --git a/src/modules/Makefile.am b/src/modules/Makefile.am index 60a79ff..1444a3d 100644 --- a/src/modules/Makefile.am +++ b/src/modules/Makefile.am @@ -1,7 +1,7 @@ install_libs = common_cflags = -I$(top_srcdir)/include -I$(top_builddir)/include -common_cflags += $(GTHREAD_CFLAGS) $(GMODULE_CFLAGS) $(LIBEV_CFLAGS) $(LUA_CFLAGS) +common_cflags += $(GTHREAD_CFLAGS) $(GMODULE_CFLAGS) $(LIBEV_CFLAGS) $(LUA_CFLAGS) $(OPENSSL_CFLAGS) common_libadd = $(GTHREAD_LIBS) $(GMODULE_LIBS) $(LIBEV_LIBS) $(LUA_LIBS) common_ldflags = -module -export-dynamic -avoid-version -no-undefined $(common_libadd) @@ -57,6 +57,11 @@ libmod_fortune_la_SOURCES = mod_fortune.c libmod_fortune_la_LDFLAGS = $(common_ldflags) # libmod_fortune_la_LIBADD = $(common_libadd) +install_libs += libmod_openssl.la +libmod_openssl_la_SOURCES = mod_openssl.c +libmod_openssl_la_LDFLAGS = $(common_ldflags) $(OPENSSL_LIBS) +# libmod_openssl_la_LIBADD = $(common_libadd) + install_libs += libmod_redirect.la libmod_redirect_la_SOURCES = mod_redirect.c libmod_redirect_la_LDFLAGS = $(common_ldflags) diff --git a/src/modules/mod_openssl.c b/src/modules/mod_openssl.c new file mode 100644 index 0000000..fab206c --- /dev/null +++ b/src/modules/mod_openssl.c @@ -0,0 +1,493 @@ +/* + * mod_openssl - ssl support + * + * Description: + * mod_openssl listens on separate sockets for ssl connections (https://...) + * + * Setups: + * openssl - setup a ssl socket; takes a hash of following parameters: + * listen - (mandatory) the socket address (same as standard listen) + * pemfile - (mandatory) contains key and direct certificate for the key (PEM format) + * ca-file - contains certificate chain + * ciphers - contains comma separated list of allowed ciphers + * allow-ssl2 - boolean option to allow ssl2 (disabled by default) + * + * Example config: + * setup openssl [ "listen": "0.0.0.0:8443", "pemfile": "server.pem" ]; + * setup openssl [ "listen": "[::]:8443", "pemfile": "server.pem" ]; + * + * Author: + * Copyright (c) 2009 Stefan Bühler + */ + +#include +#include + +#include +#include +#include + +LI_API gboolean mod_openssl_init(liModules *mods, liModule *mod); +LI_API gboolean mod_openssl_free(liModules *mods, liModule *mod); + + +typedef struct openssl_connection_ctx openssl_connection_ctx; +typedef struct openssl_context openssl_context; + +struct openssl_connection_ctx { + SSL *ssl; + GByteArray *reuse_read_buffer; +}; + +struct openssl_context { + SSL_CTX *ssl_ctx; +}; + +static gboolean openssl_con_new(liConnection *con) { + liServer *srv = con->srv; + openssl_context *ctx = con->srv_sock->data; + openssl_connection_ctx *conctx = g_slice_new0(openssl_connection_ctx); + + if (NULL == (conctx->ssl = SSL_new(ctx->ssl_ctx))) { + ERROR(srv, "SSL_new: %s", ERR_error_string(ERR_get_error(), NULL)); + goto fail; + } + + SSL_set_accept_state(conctx->ssl); + + if (1 != (SSL_set_fd(conctx->ssl, con->sock_watcher.fd))) { + ERROR(srv, "SSL_set_fd: %s", ERR_error_string(ERR_get_error(), NULL)); + goto fail; + } + + con->srv_sock_data = conctx; + + return TRUE; + +fail: + if (conctx->ssl) { + SSL_free(conctx->ssl); + } + + g_slice_free(openssl_connection_ctx, conctx); + + return FALSE; +} + +static void openssl_con_close(liConnection *con) { + openssl_connection_ctx *conctx = con->srv_sock_data; + + if (conctx->ssl) { + SSL_shutdown(conctx->ssl); /* TODO: wait for something??? */ + SSL_free(conctx->ssl); + conctx->ssl = FALSE; + } +} + +static liNetworkStatus openssl_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; + openssl_connection_ctx *conctx = con->srv_sock_data; + + do { + if (0 == cq->length) + return LI_NETWORK_STATUS_SUCCESS; + + ci = chunkqueue_iter(cq); + switch (li_chunkiter_read(con->mainvr, ci, 0, blocksize, &block_data, &block_len)) { + case LI_HANDLER_GO_ON: + break; + case LI_HANDLER_ERROR: + default: + return LI_NETWORK_STATUS_FATAL_ERROR; + } + + /** + * SSL_write man-page + * + * WARNING + * When an SSL_write() operation has to be repeated because of + * SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE, it must be + * repeated with the same arguments. + * + */ + + ERR_clear_error(); + if ((r = SSL_write(conctx->ssl, block_data, block_len)) <= 0) { + int ssl_r; + unsigned long err; + + switch ((ssl_r = SSL_get_error(conctx->ssl, r))) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return LI_NETWORK_STATUS_WAIT_FOR_EVENT; + case SSL_ERROR_SYSCALL: + /* perhaps we have error waiting in our error-queue */ + if (0 != (err = ERR_get_error())) { + do { + VR_ERROR(con->mainvr, "SSL_write: %s", + ERR_error_string(err, NULL)); + } while (0 != (err = ERR_get_error())); + } else if (r == -1) { + /* no, but we have errno */ + switch(errno) { + case EPIPE: + case ECONNRESET: + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + default: + VR_ERROR(con->mainvr, "SSL_write: %s", + g_strerror(errno)); + break; + } + } else { + /* neither error-queue nor errno ? */ + VR_ERROR(con->mainvr, "SSL_write: %s", + "Unexpected eof"); + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + } + + return LI_NETWORK_STATUS_FATAL_ERROR; + case SSL_ERROR_ZERO_RETURN: + /* clean shutdown on the remote side */ + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + default: + while (0 != (err = ERR_get_error())) { + VR_ERROR(con->mainvr, "SSL_write: %s", + ERR_error_string(err, NULL)); + } + + return LI_NETWORK_STATUS_FATAL_ERROR; + } + } + + li_chunkqueue_skip(cq, r); + write_max -= r; + } while (r == block_len && write_max > 0); + + return LI_NETWORK_STATUS_SUCCESS; +} + +static liNetworkStatus openssl_con_read(liConnection *con) { + liChunkQueue *cq = con->raw_in; + openssl_connection_ctx *conctx = con->srv_sock_data; + GByteArray *buf; + + const ssize_t blocksize = 16*1024; /* 16k */ + off_t max_read = 16 * blocksize; /* 256k */ + ssize_t r; + off_t len = 0; + + 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, "%s", "li_network_read: fd should be disabled as chunkqueue is already full"); + } + } + } + + buf = conctx->reuse_read_buffer; + conctx->reuse_read_buffer = NULL; + + do { + ERR_clear_error(); + + if (!buf) { + buf = g_byte_array_new(); + g_byte_array_set_size(buf, blocksize); + } + + r = SSL_read(conctx->ssl, buf->data, buf->len); + if (r < 0) { + int oerrno = errno, err; + gboolean was_fatal; + + err = SSL_get_error(conctx->ssl, r); + + if (SSL_ERROR_WANT_READ == err || SSL_ERROR_WANT_WRITE == err) { + conctx->reuse_read_buffer = buf; + return LI_NETWORK_STATUS_WAIT_FOR_EVENT; + } + g_byte_array_free(buf, TRUE); + buf = NULL; + + switch (err) { + case SSL_ERROR_SYSCALL: + /** + * man SSL_get_error() + * + * SSL_ERROR_SYSCALL + * Some I/O error occurred. The OpenSSL error queue may contain more + * information on the error. If the error queue is empty (i.e. + * ERR_get_error() returns 0), ret can be used to find out more about + * the error: If ret == 0, an EOF was observed that violates the + * protocol. If ret == -1, the underlying BIO reported an I/O error + * (for socket I/O on Unix systems, consult errno for details). + * + */ + while (0 != (err = ERR_get_error())) { + VR_ERROR(con->mainvr, "SSL_read: %s", + ERR_error_string(err, NULL)); + } + + switch (oerrno) { + case EPIPE: + case ECONNRESET: + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + } + + VR_ERROR(con->mainvr, "SSL_read: %s", g_strerror(oerrno)); + + break; + case SSL_ERROR_ZERO_RETURN: + /* clean shutdown on the remote side */ + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + default: + was_fatal = FALSE; + + while((err = ERR_get_error())) { + switch (ERR_GET_REASON(err)) { + case SSL_R_SSL_HANDSHAKE_FAILURE: + case SSL_R_TLSV1_ALERT_UNKNOWN_CA: + case SSL_R_SSLV3_ALERT_CERTIFICATE_UNKNOWN: + case SSL_R_SSLV3_ALERT_BAD_CERTIFICATE: + /* TODO: if (!con->conf.log_ssl_noise) */ continue; + break; + default: + was_fatal = TRUE; + break; + } + /* get all errors from the error-queue */ + VR_ERROR(con->mainvr, "SSL_read: %s", + ERR_error_string(err, NULL)); + } + if (!was_fatal) return LI_NETWORK_STATUS_CONNECTION_CLOSE; + break; + } + + return LI_NETWORK_STATUS_FATAL_ERROR; + } else if (r == 0) { + g_byte_array_free(buf, TRUE); + return LI_NETWORK_STATUS_CONNECTION_CLOSE; + } + + g_byte_array_set_size(buf, r); + li_chunkqueue_append_bytearr(cq, buf); + buf = NULL; + len += r; + } while (r == blocksize && len < max_read); + + return LI_NETWORK_STATUS_SUCCESS; +} + +static void openssl_sock_release(liServerSocket *srv_sock) { + openssl_context *ctx = srv_sock->data; + + if (!ctx) return; + + SSL_CTX_free(ctx->ssl_ctx); + g_slice_free(openssl_context, ctx); +} + +static void openssl_setup_listen_cb(liServer *srv, int fd, gpointer data) { + openssl_context *ctx = data; + liServerSocket *srv_sock; + UNUSED(data); + + if (-1 == fd) { + SSL_CTX_free(ctx->ssl_ctx); + g_slice_free(openssl_context, ctx); + return; + } + + srv_sock = li_server_listen(srv, fd); + + srv_sock->data = ctx; + + srv_sock->write_cb = openssl_con_write; + srv_sock->read_cb = openssl_con_read; + srv_sock->new_cb = openssl_con_new; + srv_sock->close_cb = openssl_con_close; + srv_sock->release_cb = openssl_sock_release; +} + +static gboolean openssl_setup(liServer *srv, liPlugin* p, liValue *val) { + openssl_context *ctx; + GHashTableIter hti; + gpointer hkey, hvalue; + GString *htkey; + liValue *htval; + + /* options */ + const char *pemfile = NULL, *ca_file = NULL, *ciphers = NULL; + GString *ipstr = NULL; + gboolean allow_ssl2 = FALSE; + + UNUSED(p); + + if (val->type != LI_VALUE_HASH) { + ERROR(srv, "%s", "openssl expects a hash as parameter"); + return FALSE; + } + + g_hash_table_iter_init(&hti, val->data.hash); + while (g_hash_table_iter_next(&hti, &hkey, &hvalue)) { + htkey = hkey; htval = hvalue; + + if (g_str_equal(htkey->str, "listen")) { + if (htval->type != LI_VALUE_STRING) { + ERROR(srv, "%s", "openssl pemfile expects a string as parameter"); + return FALSE; + } + ipstr = htval->data.string; + } else if (g_str_equal(htkey->str, "pemfile")) { + if (htval->type != LI_VALUE_STRING) { + ERROR(srv, "%s", "openssl pemfile expects a string as parameter"); + return FALSE; + } + pemfile = htval->data.string->str; + } else if (g_str_equal(htkey->str, "ca-file")) { + if (htval->type != LI_VALUE_STRING) { + ERROR(srv, "%s", "openssl ca-file expects a string as parameter"); + return FALSE; + } + ca_file = htval->data.string->str; + } else if (g_str_equal(htkey->str, "ciphers")) { + if (htval->type != LI_VALUE_STRING) { + ERROR(srv, "%s", "openssl ciphers expects a string as parameter"); + return FALSE; + } + ciphers = htval->data.string->str; + } else if (g_str_equal(htkey->str, "allow-ssl2")) { + if (htval->type != LI_VALUE_BOOLEAN) { + ERROR(srv, "%s", "openssl allow-ssl2 expects a boolean as parameter"); + return FALSE; + } + allow_ssl2 = htval->data.boolean; + } + } + + if (!ipstr) { + ERROR(srv, "%s", "openssl needs a listen parameter"); + return FALSE; + } + + if (!pemfile) { + ERROR(srv, "%s", "openssl needs a pemfile"); + return FALSE; + } + + ctx = g_slice_new0(openssl_context); + + if (NULL == (ctx->ssl_ctx = SSL_CTX_new(SSLv23_server_method()))) { + ERROR(srv, "SSL_CTX_new: %s", ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + + if (!allow_ssl2) { + /* disable SSLv2 */ + if (SSL_OP_NO_SSLv2 != SSL_CTX_set_options(ctx->ssl_ctx, SSL_OP_NO_SSLv2)) { + ERROR(srv, "SSL_CTX_set_options(SSL_OP_NO_SSLv2): %s", ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + } + + if (ciphers) { + /* Disable support for low encryption ciphers */ + if (SSL_CTX_set_cipher_list(ctx->ssl_ctx, ciphers) != 1) { + ERROR(srv, "SSL_CTX_set_cipher_list('%s'): %s", ciphers, ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + } + + if (ca_file) { + if (1 != SSL_CTX_load_verify_locations(ctx->ssl_ctx, ca_file, NULL)) { + ERROR(srv, "SSL_CTX_load_verify_locations('%s'): %s", ca_file, ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + } + + if (SSL_CTX_use_certificate_file(ctx->ssl_ctx, pemfile, SSL_FILETYPE_PEM) < 0) { + ERROR(srv, "SSL_CTX_use_certificate_file('%s'): %s", pemfile, + ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + + if (SSL_CTX_use_PrivateKey_file (ctx->ssl_ctx, pemfile, SSL_FILETYPE_PEM) < 0) { + ERROR(srv, "SSL_CTX_use_PrivateKey_file('%s'): %s", pemfile, + ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + + if (SSL_CTX_check_private_key(ctx->ssl_ctx) != 1) { + ERROR(srv, "SSL: Private key '%s' does not match the certificate public key, reason: %s", pemfile, + ERR_error_string(ERR_get_error(), NULL)); + goto error_free_socket; + } + + SSL_CTX_set_default_read_ahead(ctx->ssl_ctx, 1); + SSL_CTX_set_mode(ctx->ssl_ctx, SSL_CTX_get_mode(ctx->ssl_ctx) | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + li_angel_listen(srv, ipstr, openssl_setup_listen_cb, ctx); + + return TRUE; + +error_free_socket: + if (ctx) { + if (ctx->ssl_ctx) SSL_CTX_free(ctx->ssl_ctx); + g_slice_free(openssl_context, ctx); + } + + return FALSE; +} + +static const liPluginOption options[] = { + { NULL, 0, NULL, NULL, NULL } +}; + +static const liPluginAction actions[] = { + { NULL, NULL } +}; + +static const liPluginSetup setups[] = { + { "openssl", openssl_setup }, + + { NULL, NULL } +}; + + +static void plugin_init(liServer *srv, liPlugin *p) { + UNUSED(srv); + + p->options = options; + p->actions = actions; + p->setups = setups; +} + +gboolean mod_openssl_init(liModules *mods, liModule *mod) { + MODULE_VERSION_CHECK(mods); + + SSL_load_error_strings(); + SSL_library_init(); + + if (0 == RAND_status()) { + ERROR(mods->main, "SSL: %s", "not enough entropy in the pool"); + return FALSE; + } + + mod->config = li_plugin_register(mods->main, "mod_openssl", plugin_init); + + return mod->config != NULL; +} + +gboolean mod_openssl_free(liModules *mods, liModule *mod) { + if (mod->config) + li_plugin_free(mods->main, mod->config); + + return TRUE; +}