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,