AST-2014-007: Fix DOS by consuming the number of allowed HTTP connections.
[asterisk/asterisk.git] / main / tcptls.c
index 3a8e412..076f94b 100644 (file)
@@ -50,102 +50,483 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/astobj2.h"
 #include "asterisk/pbx.h"
 
-/*! \brief
- * replacement read/write functions for SSL support.
- * We use wrappers rather than SSL_read/SSL_write directly so
- * we can put in some debugging.
- */
+/*! ao2 object used for the FILE stream fopencookie()/funopen() cookie. */
+struct ast_tcptls_stream {
+       /*! SSL state if not NULL */
+       SSL *ssl;
+       /*!
+        * \brief Start time from when an I/O sequence must complete
+        * by struct ast_tcptls_stream.timeout.
+        *
+        * \note If struct ast_tcptls_stream.start.tv_sec is zero then
+        * start time is the current I/O request.
+        */
+       struct timeval start;
+       /*!
+        * \brief The socket returned by accept().
+        *
+        * \note Set to -1 if the stream is closed.
+        */
+       int fd;
+       /*!
+        * \brief Timeout in ms relative to struct ast_tcptls_stream.start
+        * to wait for an event on struct ast_tcptls_stream.fd.
+        *
+        * \note Set to -1 to disable timeout.
+        * \note The socket needs to be set to non-blocking for the timeout
+        * feature to work correctly.
+        */
+       int timeout;
+};
 
-#ifdef DO_SSL
-static HOOK_T ssl_read(void *cookie, char *buf, LEN_T len)
+void ast_tcptls_stream_set_timeout_disable(struct ast_tcptls_stream *stream)
 {
-       int i = SSL_read(cookie, buf, len-1);
-#if 0
-       if (i >= 0) {
-               buf[i] = '\0';
-       }
-       ast_verb(0, "ssl read size %d returns %d <%s>\n", (int)len, i, buf);
-#endif
-       return i;
+       ast_assert(stream != NULL);
+
+       stream->timeout = -1;
 }
 
-static HOOK_T ssl_write(void *cookie, const char *buf, LEN_T len)
+void ast_tcptls_stream_set_timeout_inactivity(struct ast_tcptls_stream *stream, int timeout)
 {
-#if 0
-       char *s = ast_alloca(len+1);
+       ast_assert(stream != NULL);
 
-       strncpy(s, buf, len);
-       s[len] = '\0';
-       ast_verb(0, "ssl write size %d <%s>\n", (int)len, s);
-#endif
-       return SSL_write(cookie, buf, len);
+       stream->start.tv_sec = 0;
+       stream->timeout = timeout;
 }
 
-static int ssl_close(void *cookie)
+void ast_tcptls_stream_set_timeout_sequence(struct ast_tcptls_stream *stream, struct timeval start, int timeout)
 {
-       int cookie_fd = SSL_get_fd(cookie);
-       int ret;
+       ast_assert(stream != NULL);
 
-       if (cookie_fd > -1) {
-               /*
-                * According to the TLS standard, it is acceptable for an application to only send its shutdown
-                * alert and then close the underlying connection without waiting for the peer's response (this
-                * way resources can be saved, as the process can already terminate or serve another connection).
-                */
-               if ((ret = SSL_shutdown(cookie)) < 0) {
-                       ast_log(LOG_ERROR, "SSL_shutdown() failed: %d\n", SSL_get_error(cookie, ret));
+       stream->start = start;
+       stream->timeout = timeout;
+}
+
+/*!
+ * \internal
+ * \brief fopencookie()/funopen() stream read function.
+ *
+ * \param cookie Stream control data.
+ * \param buf Where to put read data.
+ * \param size Size of the buffer.
+ *
+ * \retval number of bytes put into buf.
+ * \retval 0 on end of file.
+ * \retval -1 on error.
+ */
+static HOOK_T tcptls_stream_read(void *cookie, char *buf, LEN_T size)
+{
+       struct ast_tcptls_stream *stream = cookie;
+       struct timeval start;
+       int ms;
+       int res;
+
+       if (!size) {
+               /* You asked for no data you got no data. */
+               return 0;
+       }
+
+       if (!stream || stream->fd == -1) {
+               errno = EBADF;
+               return -1;
+       }
+
+       if (stream->start.tv_sec) {
+               start = stream->start;
+       } else {
+               start = ast_tvnow();
+       }
+
+#if defined(DO_SSL)
+       if (stream->ssl) {
+               for (;;) {
+                       res = SSL_read(stream->ssl, buf, size);
+                       if (0 < res) {
+                               /* We read some payload data. */
+                               return res;
+                       }
+                       switch (SSL_get_error(stream->ssl, res)) {
+                       case SSL_ERROR_ZERO_RETURN:
+                               /* Report EOF for a shutdown */
+                               ast_debug(1, "TLS clean shutdown alert reading data\n");
+                               return 0;
+                       case SSL_ERROR_WANT_READ:
+                               while ((ms = ast_remaining_ms(start, stream->timeout))) {
+                                       res = ast_wait_for_input(stream->fd, ms);
+                                       if (0 < res) {
+                                               /* Socket is ready to be read. */
+                                               break;
+                                       }
+                                       if (res < 0) {
+                                               if (errno == EINTR || errno == EAGAIN) {
+                                                       /* Try again. */
+                                                       continue;
+                                               }
+                                               ast_debug(1, "TLS socket error waiting for read data: %s\n",
+                                                       strerror(errno));
+                                               return -1;
+                                       }
+                               }
+                               break;
+                       case SSL_ERROR_WANT_WRITE:
+                               while ((ms = ast_remaining_ms(start, stream->timeout))) {
+                                       res = ast_wait_for_output(stream->fd, ms);
+                                       if (0 < res) {
+                                               /* Socket is ready to be written. */
+                                               break;
+                                       }
+                                       if (res < 0) {
+                                               if (errno == EINTR || errno == EAGAIN) {
+                                                       /* Try again. */
+                                                       continue;
+                                               }
+                                               ast_debug(1, "TLS socket error waiting for write space: %s\n",
+                                                       strerror(errno));
+                                               return -1;
+                                       }
+                               }
+                               break;
+                       default:
+                               /* Report EOF for an undecoded SSL or transport error. */
+                               ast_debug(1, "TLS transport or SSL error reading data\n");
+                               return 0;
+                       }
+                       if (!ms) {
+                               /* Report EOF for a timeout */
+                               ast_debug(1, "TLS timeout reading data\n");
+                               return 0;
+                       }
+               }
+       }
+#endif /* defined(DO_SSL) */
+
+       for (;;) {
+               res = read(stream->fd, buf, size);
+               if (0 <= res) {
+                       return res;
                }
+               if (errno != EINTR && errno != EAGAIN) {
+                       /* Not a retryable error. */
+                       ast_debug(1, "TCP socket error reading data: %s\n",
+                               strerror(errno));
+                       return -1;
+               }
+               ms = ast_remaining_ms(start, stream->timeout);
+               if (!ms) {
+                       /* Report EOF for a timeout */
+                       ast_debug(1, "TCP timeout reading data\n");
+                       return 0;
+               }
+               ast_wait_for_input(stream->fd, ms);
+       }
+}
+
+/*!
+ * \internal
+ * \brief fopencookie()/funopen() stream write function.
+ *
+ * \param cookie Stream control data.
+ * \param buf Where to get data to write.
+ * \param size Size of the buffer.
+ *
+ * \retval number of bytes written from buf.
+ * \retval -1 on error.
+ */
+static HOOK_T tcptls_stream_write(void *cookie, const char *buf, LEN_T size)
+{
+       struct ast_tcptls_stream *stream = cookie;
+       struct timeval start;
+       int ms;
+       int res;
+       int written;
+       int remaining;
+
+       if (!size) {
+               /* You asked to write no data you wrote no data. */
+               return 0;
+       }
+
+       if (!stream || stream->fd == -1) {
+               errno = EBADF;
+               return -1;
+       }
+
+       if (stream->start.tv_sec) {
+               start = stream->start;
+       } else {
+               start = ast_tvnow();
+       }
 
-               if (!((SSL*)cookie)->server) {
-                       /* For client threads, ensure that the error stack is cleared */
-                       ERR_remove_state(0);
+#if defined(DO_SSL)
+       if (stream->ssl) {
+               written = 0;
+               remaining = size;
+               for (;;) {
+                       res = SSL_write(stream->ssl, buf + written, remaining);
+                       if (res == remaining) {
+                               /* Everything was written. */
+                               return size;
+                       }
+                       if (0 < res) {
+                               /* Successfully wrote part of the buffer.  Try to write the rest. */
+                               written += res;
+                               remaining -= res;
+                               continue;
+                       }
+                       switch (SSL_get_error(stream->ssl, res)) {
+                       case SSL_ERROR_ZERO_RETURN:
+                               ast_debug(1, "TLS clean shutdown alert writing data\n");
+                               if (written) {
+                                       /* Report partial write. */
+                                       return written;
+                               }
+                               errno = EBADF;
+                               return -1;
+                       case SSL_ERROR_WANT_READ:
+                               ms = ast_remaining_ms(start, stream->timeout);
+                               if (!ms) {
+                                       /* Report partial write. */
+                                       ast_debug(1, "TLS timeout writing data (want read)\n");
+                                       return written;
+                               }
+                               ast_wait_for_input(stream->fd, ms);
+                               break;
+                       case SSL_ERROR_WANT_WRITE:
+                               ms = ast_remaining_ms(start, stream->timeout);
+                               if (!ms) {
+                                       /* Report partial write. */
+                                       ast_debug(1, "TLS timeout writing data (want write)\n");
+                                       return written;
+                               }
+                               ast_wait_for_output(stream->fd, ms);
+                               break;
+                       default:
+                               /* Undecoded SSL or transport error. */
+                               ast_debug(1, "TLS transport or SSL error writing data\n");
+                               if (written) {
+                                       /* Report partial write. */
+                                       return written;
+                               }
+                               errno = EBADF;
+                               return -1;
+                       }
                }
+       }
+#endif /* defined(DO_SSL) */
 
-               SSL_free(cookie);
-               /* adding shutdown(2) here has no added benefit */
-               if (close(cookie_fd)) {
+       written = 0;
+       remaining = size;
+       for (;;) {
+               res = write(stream->fd, buf + written, remaining);
+               if (res == remaining) {
+                       /* Yay everything was written. */
+                       return size;
+               }
+               if (0 < res) {
+                       /* Successfully wrote part of the buffer.  Try to write the rest. */
+                       written += res;
+                       remaining -= res;
+                       continue;
+               }
+               if (errno != EINTR && errno != EAGAIN) {
+                       /* Not a retryable error. */
+                       ast_debug(1, "TCP socket error writing: %s\n", strerror(errno));
+                       if (written) {
+                               return written;
+                       }
+                       return -1;
+               }
+               ms = ast_remaining_ms(start, stream->timeout);
+               if (!ms) {
+                       /* Report partial write. */
+                       ast_debug(1, "TCP timeout writing data\n");
+                       return written;
+               }
+               ast_wait_for_output(stream->fd, ms);
+       }
+}
+
+/*!
+ * \internal
+ * \brief fopencookie()/funopen() stream close function.
+ *
+ * \param cookie Stream control data.
+ *
+ * \retval 0 on success.
+ * \retval -1 on error.
+ */
+static int tcptls_stream_close(void *cookie)
+{
+       struct ast_tcptls_stream *stream = cookie;
+
+       if (!stream) {
+               errno = EBADF;
+               return -1;
+       }
+
+       if (stream->fd != -1) {
+#if defined(DO_SSL)
+               if (stream->ssl) {
+                       int res;
+
+                       /*
+                        * According to the TLS standard, it is acceptable for an
+                        * application to only send its shutdown alert and then
+                        * close the underlying connection without waiting for
+                        * the peer's response (this way resources can be saved,
+                        * as the process can already terminate or serve another
+                        * connection).
+                        */
+                       res = SSL_shutdown(stream->ssl);
+                       if (res < 0) {
+                               ast_log(LOG_ERROR, "SSL_shutdown() failed: %d\n",
+                                       SSL_get_error(stream->ssl, res));
+                       }
+
+                       if (!stream->ssl->server) {
+                               /* For client threads, ensure that the error stack is cleared */
+                               ERR_remove_state(0);
+                       }
+
+                       SSL_free(stream->ssl);
+                       stream->ssl = NULL;
+               }
+#endif /* defined(DO_SSL) */
+
+               /*
+                * Issuing shutdown() is necessary here to avoid a race
+                * condition where the last data written may not appear
+                * in the TCP stream.  See ASTERISK-23548
+                */
+               shutdown(stream->fd, SHUT_RDWR);
+               if (close(stream->fd)) {
                        ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno));
                }
+               stream->fd = -1;
        }
+       ao2_t_ref(stream, -1, "Closed tcptls stream cookie");
+
        return 0;
 }
-#endif /* DO_SSL */
+
+/*!
+ * \internal
+ * \brief fopencookie()/funopen() stream destructor function.
+ *
+ * \param cookie Stream control data.
+ *
+ * \return Nothing
+ */
+static void tcptls_stream_dtor(void *cookie)
+{
+       struct ast_tcptls_stream *stream = cookie;
+
+       ast_assert(stream->fd == -1);
+}
+
+/*!
+ * \internal
+ * \brief fopencookie()/funopen() stream allocation function.
+ *
+ * \retval stream_cookie on success.
+ * \retval NULL on error.
+ */
+static struct ast_tcptls_stream *tcptls_stream_alloc(void)
+{
+       struct ast_tcptls_stream *stream;
+
+       stream = ao2_alloc_options(sizeof(*stream), tcptls_stream_dtor,
+               AO2_ALLOC_OPT_LOCK_NOLOCK);
+       if (stream) {
+               stream->fd = -1;
+               stream->timeout = -1;
+       }
+       return stream;
+}
+
+/*!
+ * \internal
+ * \brief Open a custom FILE stream for tcptls.
+ *
+ * \param stream Stream cookie control data.
+ * \param ssl SSL state if not NULL.
+ * \param fd Socket file descriptor.
+ * \param timeout ms to wait for an event on fd. -1 if timeout disabled.
+ *
+ * \retval fp on success.
+ * \retval NULL on error.
+ */
+static FILE *tcptls_stream_fopen(struct ast_tcptls_stream *stream, SSL *ssl, int fd, int timeout)
+{
+       FILE *fp;
+
+#if defined(HAVE_FOPENCOOKIE)  /* the glibc/linux interface */
+       static const cookie_io_functions_t cookie_funcs = {
+               tcptls_stream_read,
+               tcptls_stream_write,
+               NULL,
+               tcptls_stream_close
+       };
+#endif /* defined(HAVE_FOPENCOOKIE) */
+
+       if (fd == -1) {
+               /* Socket not open. */
+               return NULL;
+       }
+
+       stream->ssl = ssl;
+       stream->fd = fd;
+       stream->timeout = timeout;
+       ao2_t_ref(stream, +1, "Opening tcptls stream cookie");
+
+#if defined(HAVE_FUNOPEN)      /* the BSD interface */
+       fp = funopen(stream, tcptls_stream_read, tcptls_stream_write, NULL,
+               tcptls_stream_close);
+#elif defined(HAVE_FOPENCOOKIE)        /* the glibc/linux interface */
+       fp = fopencookie(stream, "w+", cookie_funcs);
+#else
+       /* could add other methods here */
+       ast_debug(2, "No stream FILE methods attempted!\n");
+       fp = NULL;
+#endif
+
+       if (!fp) {
+               stream->fd = -1;
+               ao2_t_ref(stream, -1, "Failed to open tcptls stream cookie");
+       }
+       return fp;
+}
 
 HOOK_T ast_tcptls_server_read(struct ast_tcptls_session_instance *tcptls_session, void *buf, size_t count)
 {
-       if (tcptls_session->fd == -1) {
-               ast_log(LOG_ERROR, "server_read called with an fd of -1\n");
+       if (!tcptls_session->stream_cookie || tcptls_session->stream_cookie->fd == -1) {
+               ast_log(LOG_ERROR, "TCP/TLS read called on invalid stream.\n");
                errno = EIO;
                return -1;
        }
 
-#ifdef DO_SSL
-       if (tcptls_session->ssl) {
-               return ssl_read(tcptls_session->ssl, buf, count);
-       }
-#endif
-       return read(tcptls_session->fd, buf, count);
+       return tcptls_stream_read(tcptls_session->stream_cookie, buf, count);
 }
 
 HOOK_T ast_tcptls_server_write(struct ast_tcptls_session_instance *tcptls_session, const void *buf, size_t count)
 {
-       if (tcptls_session->fd == -1) {
-               ast_log(LOG_ERROR, "server_write called with an fd of -1\n");
+       if (!tcptls_session->stream_cookie || tcptls_session->stream_cookie->fd == -1) {
+               ast_log(LOG_ERROR, "TCP/TLS write called on invalid stream.\n");
                errno = EIO;
                return -1;
        }
 
-#ifdef DO_SSL
-       if (tcptls_session->ssl) {
-               return ssl_write(tcptls_session->ssl, buf, count);
-       }
-#endif
-       return write(tcptls_session->fd, buf, count);
+       return tcptls_stream_write(tcptls_session->stream_cookie, buf, count);
 }
 
 static void session_instance_destructor(void *obj)
 {
        struct ast_tcptls_session_instance *i = obj;
+
+       if (i->stream_cookie) {
+               ao2_t_ref(i->stream_cookie, -1, "Destroying tcptls session instance");
+               i->stream_cookie = NULL;
+       }
        ast_free(i->overflow_buf);
 }
 
@@ -177,12 +558,21 @@ static void *handle_tcptls_connection(void *data)
                return NULL;
        }
 
+       tcptls_session->stream_cookie = tcptls_stream_alloc();
+       if (!tcptls_session->stream_cookie) {
+               ast_tcptls_close_session_file(tcptls_session);
+               ao2_ref(tcptls_session, -1);
+               return NULL;
+       }
+
        /*
        * open a FILE * as appropriate.
        */
        if (!tcptls_session->parent->tls_cfg) {
-               if ((tcptls_session->f = fdopen(tcptls_session->fd, "w+"))) {
-                       if(setvbuf(tcptls_session->f, NULL, _IONBF, 0)) {
+               tcptls_session->f = tcptls_stream_fopen(tcptls_session->stream_cookie, NULL,
+                       tcptls_session->fd, -1);
+               if (tcptls_session->f) {
+                       if (setvbuf(tcptls_session->f, NULL, _IONBF, 0)) {
                                ast_tcptls_close_session_file(tcptls_session);
                        }
                }
@@ -192,19 +582,8 @@ static void *handle_tcptls_connection(void *data)
                SSL_set_fd(tcptls_session->ssl, tcptls_session->fd);
                if ((ret = ssl_setup(tcptls_session->ssl)) <= 0) {
                        ast_log(LOG_ERROR, "Problem setting up ssl connection: %s\n", ERR_error_string(ERR_get_error(), err));
-               } else {
-#if defined(HAVE_FUNOPEN)      /* the BSD interface */
-                       tcptls_session->f = funopen(tcptls_session->ssl, ssl_read, ssl_write, NULL, ssl_close);
-
-#elif defined(HAVE_FOPENCOOKIE)        /* the glibc/linux interface */
-                       static const cookie_io_functions_t cookie_funcs = {
-                               ssl_read, ssl_write, NULL, ssl_close
-                       };
-                       tcptls_session->f = fopencookie(tcptls_session->ssl, "w+", cookie_funcs);
-#else
-                       /* could add other methods here */
-                       ast_debug(2, "no tcptls_session->f methods attempted!\n");
-#endif
+               } else if ((tcptls_session->f = tcptls_stream_fopen(tcptls_session->stream_cookie,
+                       tcptls_session->ssl, tcptls_session->fd, -1))) {
                        if ((tcptls_session->client && !ast_test_flag(&tcptls_session->parent->tls_cfg->flags, AST_SSL_DONT_VERIFY_SERVER))
                                || (!tcptls_session->client && ast_test_flag(&tcptls_session->parent->tls_cfg->flags, AST_SSL_VERIFY_CLIENT))) {
                                X509 *peer;
@@ -625,21 +1004,18 @@ error:
 void ast_tcptls_close_session_file(struct ast_tcptls_session_instance *tcptls_session)
 {
        if (tcptls_session->f) {
-               /*
-                * Issuing shutdown() is necessary here to avoid a race
-                * condition where the last data written may not appear
-                * in the TCP stream.  See ASTERISK-23548
-               */
                fflush(tcptls_session->f);
-               if (tcptls_session->fd != -1) {
-                       shutdown(tcptls_session->fd, SHUT_RDWR);
-               }
                if (fclose(tcptls_session->f)) {
                        ast_log(LOG_ERROR, "fclose() failed: %s\n", strerror(errno));
                }
                tcptls_session->f = NULL;
                tcptls_session->fd = -1;
        } else if (tcptls_session->fd != -1) {
+               /*
+                * Issuing shutdown() is necessary here to avoid a race
+                * condition where the last data written may not appear
+                * in the TCP stream.  See ASTERISK-23548
+                */
                shutdown(tcptls_session->fd, SHUT_RDWR);
                if (close(tcptls_session->fd)) {
                        ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno));