Commit f9a38e1608 for asterisk.org
commit f9a38e16080e3e4d175a955f315ea0287736a2b8
Author: George Joseph <gjoseph@sangoma.com>
Date: Tue Jun 9 09:29:35 2026 -0600
res_http_websocket: Add timeout to client handshakes.
The websocket client proxy and server handshakes use ast_iostream_gets which
are blocking calls. If the outgoing connection succeeds at the TCP or TLS
layer but the proxy (if configured) or the websocket server fails to respond
to the CONNECT or GET requests, the process can hang indefinitely and escalate
to a deadlock. To address this, the handshakes are now guarded with calls to
ast_iostream_set_timeout_sequence() with the timeout set to the client's
(connection_timeout * 2) milliseconds.
In order to use ast_iostream_set_timeout_sequence(), the iostream has to be
set to non-blocking with ast_iostream_nonblock() but there was no way to
reset the stream back to blocking mode so a new API ast_iostream_blocking()
was added for it.
Tracing was also enabled in the websocket_client_handshake function for
future troubleshooting.
Resolves: #1979
diff --git a/include/asterisk/iostream.h b/include/asterisk/iostream.h
index 9600e969f1..9d88e3922f 100644
--- a/include/asterisk/iostream.h
+++ b/include/asterisk/iostream.h
@@ -148,6 +148,16 @@ int ast_iostream_wait_for_input(struct ast_iostream *stream, int timeout);
*/
void ast_iostream_nonblock(struct ast_iostream *stream);
+/*!
+ * \brief Make an iostream blocking.
+ * \since 20.21.0
+ * \since 22.11.0
+ * \since 23.5.0
+ *
+ * \param stream A pointer to an iostream
+ */
+void ast_iostream_blocking(struct ast_iostream *stream);
+
/*!
* \brief Get a pointer to an iostream's OpenSSL \c SSL structure
*
diff --git a/main/iostream.c b/main/iostream.c
index 980bbf40c1..f9e00b449b 100644
--- a/main/iostream.c
+++ b/main/iostream.c
@@ -106,6 +106,11 @@ void ast_iostream_nonblock(struct ast_iostream *stream)
ast_fd_set_flags(stream->fd, O_NONBLOCK);
}
+void ast_iostream_blocking(struct ast_iostream *stream)
+{
+ ast_fd_clear_flags(stream->fd, O_NONBLOCK);
+}
+
SSL *ast_iostream_get_ssl(struct ast_iostream *stream)
{
return stream->ssl;
diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c
index d062a226a4..1ec4120fd8 100644
--- a/res/res_http_websocket.c
+++ b/res/res_http_websocket.c
@@ -1626,11 +1626,9 @@ static enum ast_websocket_result websocket_client_handshake_get_response(
S_OR(client->options->proxy_host, "N/A"));
while (ast_iostream_gets(client->ser->stream, buf, sizeof(buf)) <= 0) {
- if (errno == EINTR || errno == EAGAIN) {
- continue;
- }
- SCOPE_EXIT_LOG_RTN_VALUE(WS_BAD_STATUS, LOG_ERROR, "%s: Unable to retrieve HTTP status line",
- client->options->uri);
+ SCOPE_EXIT_LOG_RTN_VALUE((errno == EINTR || errno == EAGAIN) ? WS_CLIENT_START_ERROR : WS_BAD_STATUS,
+ LOG_ERROR, "%s: %s waiting for HTTP status line", client->options->uri,
+ (errno == EINTR || errno == EAGAIN) ? "Timeout" : "Error");
}
status_code = ast_http_response_status_line(buf, "HTTP/1.1", 101);
@@ -1651,7 +1649,11 @@ static enum ast_websocket_result websocket_client_handshake_get_response(
if (len <= 0) {
if (errno == EINTR || errno == EAGAIN) {
- continue;
+ SCOPE_EXIT_LOG_RTN_VALUE((errno == EINTR || errno == EAGAIN) ? WS_CLIENT_START_ERROR : WS_BAD_STATUS,
+ LOG_ERROR, "%s: %s waiting for HTTP header", client->options->uri,
+ (errno == EINTR || errno == EAGAIN) ? "Timeout" : "Error");
+ } else {
+ ast_trace(-1, "%s: Blank line received\n", client->options->uri);
}
break;
}
@@ -1693,14 +1695,47 @@ static enum ast_websocket_result websocket_client_handshake_get_response(
}
}
- if (proxy) {
- res = WS_OK;
+ if (status_code == 408) {
+ res = WS_CLIENT_START_ERROR;
} else {
- res = has_upgrade && has_connection && has_accept ?
- WS_OK : WS_HEADER_MISSING;
+ if (proxy) {
+ res = WS_OK;
+ } else {
+ res = has_upgrade && has_connection && has_accept ?
+ WS_OK : WS_HEADER_MISSING;
+ }
}
- SCOPE_EXIT_RTN_VALUE(res);
+ SCOPE_EXIT_RTN_VALUE(res, "%s: Status code: %d\n", client->options->uri, status_code);
+}
+
+static void websocket_client_start_handshake_timer(struct websocket_client *client)
+{
+ /*
+ * ast_iostream_gets (called above in websocket_client_handshake_get_response) is
+ * a blocking call which means that if the TCP/TLS connection succeeds but the remote doesn't
+ * actually respond to the proxy CONNECT (if proxy is configured) or GET requests, the process
+ * can hang indefinitely and escalate to a deadlock. To get ast_iostream_gets to timeout,
+ * we need to make the following 3 calls on the iostream.
+ *
+ * Since the write of the CONNECT and/or GET request is included in the timeout, we'll
+ * double the timeout set in the websocket client's "connect_timeout" parameter to give
+ * the server enough time to respond.
+ */
+ ast_iostream_nonblock(client->ser->stream);
+ ast_iostream_set_exclusive_input(client->ser->stream, 1);
+ ast_iostream_set_timeout_sequence(client->ser->stream, ast_tvnow(), client->options->timeout * 2);
+}
+
+static void websocket_client_stop_handshake_timer(struct websocket_client *client)
+{
+ /*
+ * Once the handshake is complete, we need to undo what we did in
+ * websocket_client_start_handshake_timer.
+ */
+ ast_iostream_set_timeout_disable(client->ser->stream);
+ ast_iostream_set_exclusive_input(client->ser->stream, 0);
+ ast_iostream_blocking(client->ser->stream);
}
#define optional_header_spec "%s%s%s"
@@ -1724,6 +1759,8 @@ static enum ast_websocket_result websocket_proxy_handshake(
}
}
+ websocket_client_start_handshake_timer(client);
+
bytes_written = ast_iostream_printf(client->ser->stream,
"CONNECT %s HTTP/1.1\r\n"
"Host: %s\r\n"
@@ -1737,10 +1774,13 @@ static enum ast_websocket_result websocket_proxy_handshake(
ast_variables_destroy(auth_header);
if (bytes_written < 0) {
+ websocket_client_stop_handshake_timer(client);
SCOPE_EXIT_LOG_RTN_VALUE(WS_WRITE_ERROR, LOG_ERROR, "Failed to send handshake\n");
}
+
/* wait for a response before doing anything else */
res = websocket_client_handshake_get_response(client, 1);
+ websocket_client_stop_handshake_timer(client);
SCOPE_EXIT_RTN_VALUE(res, "%s\n", ast_websocket_result_to_str(res));
}
@@ -1751,17 +1791,19 @@ static enum ast_websocket_result websocket_client_handshake(
size_t protocols_len = 0;
struct ast_variable *auth_header = NULL;
size_t res;
+ SCOPE_ENTER(2, "%s: Handshaking with server\n", client->options->uri);
if (!ast_strlen_zero(client->userinfo)) {
auth_header = ast_http_create_basic_auth_header(client->userinfo, NULL);
if (!auth_header) {
- ast_log(LOG_ERROR, "Unable to allocate client websocket userinfo\n");
- return WS_ALLOCATE_ERROR;
+ SCOPE_EXIT_LOG_RTN_VALUE(WS_ALLOCATE_ERROR, LOG_ERROR, "Unable to allocate client websocket userinfo\n");
}
}
protocols_len = client->protocols ? strlen(client->protocols) : 0;
+ websocket_client_start_handshake_timer(client);
+
res = ast_iostream_printf(client->ser->stream,
"GET /%s HTTP/1.1\r\n"
"Sec-WebSocket-Version: %d\r\n"
@@ -1782,11 +1824,15 @@ static enum ast_websocket_result websocket_client_handshake(
ast_variables_destroy(auth_header);
if (res < 0) {
- ast_log(LOG_ERROR, "Failed to send handshake.\n");
- return WS_WRITE_ERROR;
+ websocket_client_stop_handshake_timer(client);
+ SCOPE_EXIT_LOG_RTN_VALUE(WS_WRITE_ERROR, LOG_ERROR, "Failed to send handshake\n");
}
+
/* wait for a response before doing anything else */
- return websocket_client_handshake_get_response(client, 0);
+ res = websocket_client_handshake_get_response(client, 0);
+ websocket_client_stop_handshake_timer(client);
+
+ SCOPE_EXIT_RTN_VALUE(res, "%s\n", ast_websocket_result_to_str(res));
}
static enum ast_websocket_result websocket_client_connect(struct ast_websocket *ws,