Index: channels/chan_sip.c
===================================================================
--- channels/chan_sip.c	(revision 415895)
+++ channels/chan_sip.c	(revision 415896)
@@ -3186,11 +3186,15 @@
 		goto cleanup;
 	}
 
+	ast_tcptls_stream_set_timeout_sequence(tcptls_session->stream_cookie, ast_tvnow(),
+		tcptls_session->client ? -1 : (authtimeout * 1000));
+
 	for (;;) {
 		struct ast_str *str_save;
 
 		if (!tcptls_session->client && req.authenticated && !authenticated) {
 			authenticated = 1;
+			ast_tcptls_stream_set_timeout_disable(tcptls_session->stream_cookie);
 			ast_atomic_fetchadd_int(&unauth_sessions, -1);
 		}
 
Index: UPGRADE.txt
===================================================================
--- UPGRADE.txt	(revision 415895)
+++ UPGRADE.txt	(revision 415896)
@@ -42,6 +42,12 @@
    * MixMonitorMute - call or system
    * StopMixMonitor - call or system
 
+ - Added http.conf session_inactivity timer option to close HTTP connections
+   that aren't doing anything.
+
+ - Removed the undocumented manager.conf block-sockets option.  It interferes with
+   TCP/TLS inactivity timeouts.
+
 From 12.2.0 to 12.3.0:
 
  - The asterisk command line -I option and the asterisk.conf internal_timing
Index: include/asterisk/utils.h
===================================================================
--- include/asterisk/utils.h	(revision 415895)
+++ include/asterisk/utils.h	(revision 415896)
@@ -369,6 +369,7 @@
 
 int ast_utils_init(void);
 int ast_wait_for_input(int fd, int ms);
+int ast_wait_for_output(int fd, int ms);
 
 /*!
  * \brief Try to write string, but wait no more than ms milliseconds
Index: include/asterisk/tcptls.h
===================================================================
--- include/asterisk/tcptls.h	(revision 415895)
+++ include/asterisk/tcptls.h	(revision 415896)
@@ -144,6 +144,51 @@
 	const char *name;
 };
 
+struct ast_tcptls_stream;
+
+/*!
+ * \brief Disable the TCP/TLS stream timeout timer.
+ *
+ * \param stream TCP/TLS stream control data.
+ *
+ * \return Nothing
+ */
+void ast_tcptls_stream_set_timeout_disable(struct ast_tcptls_stream *stream);
+
+/*!
+ * \brief Set the TCP/TLS stream inactivity timeout timer.
+ *
+ * \param stream TCP/TLS stream control data.
+ * \param timeout Number of milliseconds to wait for data transfer with the peer.
+ *
+ * \details This is basically how much time we are willing to spend
+ * in an I/O call before we declare the peer unresponsive.
+ *
+ * \note Setting timeout to -1 disables the timeout.
+ * \note Setting this timeout replaces the I/O sequence timeout timer.
+ *
+ * \return Nothing
+ */
+void ast_tcptls_stream_set_timeout_inactivity(struct ast_tcptls_stream *stream, int timeout);
+
+/*!
+ * \brief Set the TCP/TLS stream I/O sequence timeout timer.
+ *
+ * \param stream TCP/TLS stream control data.
+ * \param start Time the I/O sequence timer starts.
+ * \param timeout Number of milliseconds from the start time before timeout.
+ *
+ * \details This is how much time are we willing to allow the peer
+ * to complete an operation that can take several I/O calls.  The
+ * main use is as an authentication timer with us.
+ *
+ * \note Setting timeout to -1 disables the timeout.
+ * \note Setting this timeout replaces the inactivity timeout timer.
+ *
+ * \return Nothing
+ */
+void ast_tcptls_stream_set_timeout_sequence(struct ast_tcptls_stream *stream, struct timeval start, int timeout);
+
 /*! \brief 
  * describes a server instance
  */
@@ -161,6 +206,8 @@
 	 * extra data.
 	 */
 	struct ast_str *overflow_buf;
+	/*! ao2 FILE stream cookie object associated with f. */
+	struct ast_tcptls_stream *stream_cookie;
 };
 
 #if defined(HAVE_FUNOPEN)
Index: main/manager.c
===================================================================
--- main/manager.c	(revision 415895)
+++ main/manager.c	(revision 415896)
@@ -1123,7 +1123,6 @@
 #define DEFAULT_REALM		"asterisk"
 static char global_realm[MAXHOSTNAMELEN];	/*!< Default realm */
 
-static int block_sockets;
 static int unauth_sessions = 0;
 static struct stasis_subscription *acl_change_sub;
 
@@ -1770,15 +1769,7 @@
 	}
 
 	if (session->f != NULL) {
-		/*
-		 * Issuing shutdown() is necessary here to avoid a race
-		 * condition where the last data written may not appear
-		 * in the the TCP stream.  See ASTERISK-23548
-		*/
 		fflush(session->f);
-		if (session->fd != -1) {
-			shutdown(session->fd, SHUT_RDWR);
-		}
 		fclose(session->f);
 	}
 	if (eqe) {
@@ -5886,12 +5877,9 @@
 		ast_log(LOG_WARNING, "Failed to set manager tcp connection to TCP_NODELAY, getprotobyname(\"tcp\") failed\nSome manager actions may be slow to respond.\n");
 	}
 
+	/* make sure socket is non-blocking */
 	flags = fcntl(ser->fd, F_GETFL);
-	if (!block_sockets) { /* make sure socket is non-blocking */
-		flags |= O_NONBLOCK;
-	} else {
-		flags &= ~O_NONBLOCK;
-	}
+	flags |= O_NONBLOCK;
 	fcntl(ser->fd, F_SETFL, flags);
 
 	ao2_lock(session);
@@ -5917,11 +5905,17 @@
 	}
 	ao2_unlock(session);
 
+	ast_tcptls_stream_set_timeout_sequence(ser->stream_cookie,
+		ast_tvnow(), authtimeout * 1000);
+
 	astman_append(&s, "Asterisk Call Manager/%s\r\n", AMI_VERSION);	/* welcome prompt */
 	for (;;) {
 		if ((res = do_message(&s)) < 0 || s.write_error) {
 			break;
 		}
+		if (session->authenticated) {
+			ast_tcptls_stream_set_timeout_disable(ser->stream_cookie);
+		}
 	}
 	/* session is over, explain why and terminate */
 	if (session->authenticated) {
@@ -6728,6 +6722,30 @@
 	}
 }
 
+static void close_mansession_file(struct mansession *s)
+{
+	if (s->f) {
+		if (fclose(s->f)) {
+			ast_log(LOG_ERROR, "fclose() failed: %s\n", strerror(errno));
+		}
+		s->f = NULL;
+		s->fd = -1;
+	} else if (s->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(s->fd, SHUT_RDWR);
+		if (close(s->fd)) {
+			ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno));
+		}
+		s->fd = -1;
+	} else {
+		ast_log(LOG_ERROR, "Attempted to close file/file descriptor on mansession without a valid file or file descriptor.\n");
+	}
+}
+
 static void process_output(struct mansession *s, struct ast_str **out, struct ast_variable *params, enum output_format format)
 {
 	char *buf;
@@ -6755,29 +6773,7 @@
 		xml_translate(out, "", params, format);
 	}
 
-	if (s->f) {
-		/*
-		 * Issuing shutdown() is necessary here to avoid a race
-		 * condition where the last data written may not appear
-		 * in the the TCP stream.  See ASTERISK-23548
-		*/
-		if (s->fd != -1) {
-			shutdown(s->fd, SHUT_RDWR);
-		}
-		if (fclose(s->f)) {
-			ast_log(LOG_ERROR, "fclose() failed: %s\n", strerror(errno));
-		}
-		s->f = NULL;
-		s->fd = -1;
-	} else if (s->fd != -1) {
-		shutdown(s->fd, SHUT_RDWR);
-		if (close(s->fd)) {
-			ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno));
-		}
-		s->fd = -1;
-	} else {
-		ast_log(LOG_ERROR, "process output attempted to close file/file descriptor on mansession without a valid file or file descriptor.\n");
-	}
+	close_mansession_file(s);
 }
 
 static int generic_http_callback(struct ast_tcptls_session_instance *ser,
@@ -7568,7 +7564,6 @@
 	ast_cli(a->fd, FORMAT, "Timestamp events:", AST_CLI_YESNO(timestampevents));
 	ast_cli(a->fd, FORMAT, "Channel vars:", S_OR(manager_channelvars, ""));
 	ast_cli(a->fd, FORMAT, "Debug:", AST_CLI_YESNO(manager_debug));
-	ast_cli(a->fd, FORMAT, "Block sockets:", AST_CLI_YESNO(block_sockets));
 #undef FORMAT
 #undef FORMAT2
 
@@ -8171,8 +8166,6 @@
 
 		if (!strcasecmp(var->name, "enabled")) {
 			manager_enabled = ast_true(val);
-		} else if (!strcasecmp(var->name, "block-sockets")) {
-			block_sockets = ast_true(val);
 		} else if (!strcasecmp(var->name, "webenabled")) {
 			webmanager_enabled = ast_true(val);
 		} else if (!strcasecmp(var->name, "port")) {
Index: main/utils.c
===================================================================
--- main/utils.c	(revision 415895)
+++ main/utils.c	(revision 415896)
@@ -1252,14 +1252,25 @@
 int ast_wait_for_input(int fd, int ms)
 {
 	struct pollfd pfd[1];
+
 	memset(pfd, 0, sizeof(pfd));
 	pfd[0].fd = fd;
-	pfd[0].events = POLLIN|POLLPRI;
+	pfd[0].events = POLLIN | POLLPRI;
 	return ast_poll(pfd, 1, ms);
 }
 
-static int ast_wait_for_output(int fd, int timeoutms)
+int ast_wait_for_output(int fd, int ms)
 {
+	struct pollfd pfd[1];
+
+	memset(pfd, 0, sizeof(pfd));
+	pfd[0].fd = fd;
+	pfd[0].events = POLLOUT;
+	return ast_poll(pfd, 1, ms);
+}
+
+static int wait_for_output(int fd, int timeoutms)
+{
 	struct pollfd pfd = {
 		.fd = fd,
 		.events = POLLOUT,
@@ -1318,7 +1329,7 @@
 	int elapsed = 0;
 
 	while (len) {
-		if (ast_wait_for_output(fd, timeoutms - elapsed)) {
+		if (wait_for_output(fd, timeoutms - elapsed)) {
 			return -1;
 		}
 
@@ -1359,7 +1370,7 @@
 	int elapsed = 0;
 
 	while (len) {
-		if (ast_wait_for_output(fd, timeoutms - elapsed)) {
+		if (wait_for_output(fd, timeoutms - elapsed)) {
 			/* poll returned a fatal error, so bail out immediately. */
 			return -1;
 		}
Index: main/http.c
===================================================================
--- main/http.c	(revision 415895)
+++ main/http.c	(revision 415896)
@@ -71,6 +71,7 @@
 #define DEFAULT_PORT 8088
 #define DEFAULT_TLS_PORT 8089
 #define DEFAULT_SESSION_LIMIT 100
+#define DEFAULT_SESSION_INACTIVITY 30000	/* (ms) Idle time waiting for data. */
 
 /* See http.h for more information about the SSL implementation */
 #if defined(HAVE_OPENSSL) && (defined(HAVE_FUNOPEN) || defined(HAVE_FOPENCOOKIE))
@@ -78,6 +79,7 @@
 #endif
 
 static int session_limit = DEFAULT_SESSION_LIMIT;
+static int session_inactivity = DEFAULT_SESSION_INACTIVITY;
 static int session_count = 0;
 
 static struct ast_tls_config http_tls_cfg;
@@ -1240,6 +1242,7 @@
 	enum ast_http_method http_method = AST_HTTP_UNKNOWN;
 	const char *transfer_encoding;
 	int remaining_headers;
+	int flags;
 	struct protoent *p;
 
 	if (ast_atomic_fetchadd_int(&session_count, +1) >= session_limit) {
@@ -1261,7 +1264,14 @@
 		ast_log(LOG_WARNING, "Some HTTP requests may be slow to respond.\n");
 	}
 
-	if (!fgets(buf, sizeof(buf), ser->f)) {
+	/* make sure socket is non-blocking */
+	flags = fcntl(ser->fd, F_GETFL);
+	flags |= O_NONBLOCK;
+	fcntl(ser->fd, F_SETFL, flags);
+
+	ast_tcptls_stream_set_timeout_inactivity(ser->stream_cookie, session_inactivity);
+
+	if (!fgets(buf, sizeof(buf), ser->f) || feof(ser->f)) {
 		goto done;
 	}
 
@@ -1301,12 +1311,19 @@
 
 	/* process "Request Headers" lines */
 	remaining_headers = MAX_HTTP_REQUEST_HEADERS;
-	while (fgets(header_line, sizeof(header_line), ser->f)) {
-		char *name, *value;
+	for (;;) {
+		char *name;
+		char *value;
 
+		if (!fgets(header_line, sizeof(header_line), ser->f) || feof(ser->f)) {
+			ast_http_error(ser, 400, "Bad Request", "Timeout");
+			goto done;
+		}
+
 		/* Trim trailing characters */
 		ast_trim_blanks(header_line);
 		if (ast_strlen_zero(header_line)) {
+			/* A blank line ends the request header section. */
 			break;
 		}
 
@@ -1374,7 +1391,7 @@
 	ast_variables_destroy(headers);
 
 	if (ser->f) {
-		fclose(ser->f);
+		ast_tcptls_close_session_file(ser);
 	}
 	ao2_ref(ser, -1);
 	ser = NULL;
@@ -1484,6 +1501,9 @@
 
 	ast_sockaddr_setnull(&https_desc.local_address);
 
+	session_limit = DEFAULT_SESSION_LIMIT;
+	session_inactivity = DEFAULT_SESSION_INACTIVITY;
+
 	if (cfg) {
 		v = ast_variable_browse(cfg, "general");
 		for (; v; v = v->next) {
@@ -1529,6 +1549,12 @@
 					ast_log(LOG_WARNING, "Invalid %s '%s' at line %d of http.conf\n",
 							v->name, v->value, v->lineno);
 				}
+			} else if (!strcasecmp(v->name, "session_inactivity")) {
+				if (ast_parse_arg(v->value, PARSE_INT32 |PARSE_DEFAULT | PARSE_IN_RANGE,
+					&session_inactivity, DEFAULT_SESSION_INACTIVITY, 1, INT_MAX)) {
+					ast_log(LOG_WARNING, "Invalid %s '%s' at line %d of http.conf\n",
+						v->name, v->value, v->lineno);
+				}
 			} else {
 				ast_log(LOG_WARNING, "Ignoring unknown option '%s' in http.conf\n", v->name);
 			}
Index: main/tcptls.c
===================================================================
--- main/tcptls.c	(revision 415895)
+++ main/tcptls.c	(revision 415896)
@@ -50,102 +50,483 @@
 #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) */
 
-		if (!((SSL*)cookie)->server) {
-			/* For client threads, ensure that the error stack is cleared */
-			ERR_remove_state(0);
+	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);
+	}
+}
 
-		SSL_free(cookie);
-		/* adding shutdown(2) here has no added benefit */
-		if (close(cookie_fd)) {
+/*!
+ * \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 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) */
+
+	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 @@
 		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 @@
 		SSL_set_fd(tcptls_session->ssl, tcptls_session->fd);
 		if ((ret = ssl_setup(tcptls_session->ssl)) <= 0) {
 			ast_verb(2, "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 @@
 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));
Index: configs/http.conf.sample
===================================================================
--- configs/http.conf.sample	(revision 415895)
+++ configs/http.conf.sample	(revision 415896)
@@ -39,6 +39,12 @@
 ;
 ;sessionlimit=100
 ;
+; session_inactivity specifies the number of milliseconds to wait for
+; more data over the HTTP connection before closing it.
+;
+; Default: 30000
+;session_inactivity=30000
+;
 ; Whether Asterisk should serve static content from http-static
 ; Default is no.
 ;
Index: res/res_http_websocket.c
===================================================================
--- res/res_http_websocket.c	(revision 415895)
+++ res/res_http_websocket.c	(revision 415896)
@@ -750,8 +750,13 @@
 	protocol_handler->callback(session, get_vars, headers);
 	ao2_ref(protocol_handler, -1);
 
-	/* By dropping the FILE* from the session it won't get closed when the HTTP server cleans up */
+	/*
+	 * By dropping the FILE* and fd from the session the connection
+	 * won't get closed when the HTTP server cleans up because we
+	 * passed the connection to the protocol handler.
+	 */
 	ser->f = NULL;
+	ser->fd = -1;
 
 	return 0;
 }

