diff --git a/UPGRADE.txt b/UPGRADE.txt index 82dad8c2e17790a90d84f7b4ead13154fcfce504..b27ae85e47920eeefa0cabf768c6d5bb83d321fe 100644 --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -225,5 +225,13 @@ Utilities: - The refcounter program has been removed in favor of the refcounter.py script in contrib/scripts. +WebSockets: + - Added a compatibility option for ari, chan_sip, and chan_pjsip + 'websocket_write_timeout'. When a websocket connection exists where Asterisk + writes a substantial amount of data to the connected client, and the connected + client is slow to process the received data, the socket may be disconnected. + In such cases, it may be necessary to adjust this value. Default is 100 ms. + + =========================================================== =========================================================== diff --git a/channels/chan_sip.c b/channels/chan_sip.c index 5e829357fa7d6a5f85e775dd7b0b0bb710e38a62..14b460c142fe7d66e4a0be9f63fe7a0c32ab6fba 100644 --- a/channels/chan_sip.c +++ b/channels/chan_sip.c @@ -2665,6 +2665,10 @@ static void sip_websocket_callback(struct ast_websocket *session, struct ast_var goto end; } + if (ast_websocket_set_timeout(session, sip_cfg.websocket_write_timeout)) { + goto end; + } + while ((res = ast_wait_for_input(ast_websocket_fd(session), -1)) > 0) { char *payload; uint64_t payload_len; @@ -32009,6 +32013,12 @@ static int reload_config(enum channelreloadreason reason) ast_copy_string(default_parkinglot, v->value, sizeof(default_parkinglot)); } else if (!strcasecmp(v->name, "refer_addheaders")) { global_refer_addheaders = ast_true(v->value); + } else if (!strcasecmp(v->name, "websocket_write_timeout")) { + if (sscanf(v->value, "%30d", &sip_cfg.websocket_write_timeout) != 1 + || sip_cfg.websocket_write_timeout < 0) { + ast_log(LOG_WARNING, "'%s' is not a valid websocket_write_timeout value at line %d. Using default '%d'.\n", v->value, v->lineno, AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT); + sip_cfg.websocket_write_timeout = AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT; + } } } diff --git a/channels/sip/include/sip.h b/channels/sip/include/sip.h index e2ab6e19a5d00df903b0c249c9989441a81cf483..ab151de1013f7f3930e7abf053f0c29041768d78 100644 --- a/channels/sip/include/sip.h +++ b/channels/sip/include/sip.h @@ -773,6 +773,7 @@ struct sip_settings { struct ast_format_cap *caps; /*!< Supported codecs */ int tcp_enabled; int default_max_forwards; /*!< Default max forwards (SIP Anti-loop) */ + int websocket_write_timeout; /*!< Socket write timeout for websocket transports, in ms */ }; struct ast_websocket; diff --git a/configs/ari.conf.sample b/configs/ari.conf.sample index decdddc5827b4d0c5e6d9972d14366644555c1aa..59f9a44e5c3f3990cd948ab544e8c492d3c06823 100644 --- a/configs/ari.conf.sample +++ b/configs/ari.conf.sample @@ -1,19 +1,25 @@ [general] -enabled = yes ; When set to no, ARI support is disabled. -;pretty = no ; When set to yes, responses from ARI are -; ; formatted to be human readable. -;allowed_origins = ; Comma separated list of allowed origins, for -; ; Cross-Origin Resource Sharing. May be set to * to -; ; allow all origins. -;auth_realm = ; Realm to use for authentication. Defaults to Asterisk -; ; REST Interface. +enabled = yes ; When set to no, ARI support is disabled. +;pretty = no ; When set to yes, responses from ARI are +; ; formatted to be human readable. +;allowed_origins = ; Comma separated list of allowed origins, for +; ; Cross-Origin Resource Sharing. May be set to * to +; ; allow all origins. +;auth_realm = ; Realm to use for authentication. Defaults to Asterisk +; ; REST Interface. +; +; Default write timeout to set on websockets. This value may need to be adjusted +; for connections where Asterisk must write a substantial amount of data and the +; receiving clients are slow to process the received information. Value is in +; milliseconds; default is 100 ms. +;websocket_write_timeout = 100 ;[username] -;type = user ; Specifies user configuration -;read_only = no ; When set to yes, user is only authorized for -; ; read-only requests. +;type = user ; Specifies user configuration +;read_only = no ; When set to yes, user is only authorized for +; ; read-only requests. ; -;password = ; Crypted or plaintext password (see password_format). +;password = ; Crypted or plaintext password (see password_format). ; ; password_format may be set to plain (the default) or crypt. When set to crypt, ; crypt(3) is used to validate the password. A crypted password can be generated @@ -22,3 +28,4 @@ enabled = yes ; When set to no, ARI support is disabled. ; When set to plain, the password is in plaintext. ; ;password_format = plain + diff --git a/configs/pjsip.conf.sample b/configs/pjsip.conf.sample index 1bcfcb96b14ecd822414e5e26c33035e81699234..3aa05a96b3f84aac448fe265a03e5d32450f9fab 100644 --- a/configs/pjsip.conf.sample +++ b/configs/pjsip.conf.sample @@ -616,7 +616,13 @@ ; "") ;tos=0 ; Enable TOS for the signalling sent over this transport (default: "0") ;cos=0 ; Enable COS for the signalling sent over this transport (default: "0") - +;websocket_write_timeout=100 ; Default write timeout to set on websocket + ; transports. This value may need to be adjusted + ; for connections where Asterisk must write a + ; substantial amount of data and the receiving + ; clients are slow to process the received + ; information. Value is in milliseconds; default + ; is 100 ms. ;==========================CONTACT SECTION OPTIONS========================= ;[contact] diff --git a/configs/sip.conf.sample b/configs/sip.conf.sample index 1175047b3e184f68990d1799e24536cf32737fd5..010137d724c6f2ac8c3a2ddf01a249033ac81879 100644 --- a/configs/sip.conf.sample +++ b/configs/sip.conf.sample @@ -229,6 +229,12 @@ tcpbindaddr=0.0.0.0 ; IP address for TCP server to bind to (0.0.0.0 ; unauthenticated sessions that will be allowed ; to connect at any given time. (default: 100) +;websocket_write_timeout = 100 ; Default write timeout to set on websocket transports. + ; This value may need to be adjusted for connections where + ; Asterisk must write a substantial amount of data and the + ; receiving clients are slow to process the received information. + ; Value is in milliseconds; default is 100 ms. + transport=udp ; Set the default transports. The order determines the primary default transport. ; If tcpenable=no and the transport set is tcp, we will fallback to UDP. diff --git a/include/asterisk/http_websocket.h b/include/asterisk/http_websocket.h index 074ae120240bbb7e67e61f6a1e43d004fca02ac2..3e07e608b5dbc35f201ea331695871e04b8c7154 100644 --- a/include/asterisk/http_websocket.h +++ b/include/asterisk/http_websocket.h @@ -24,6 +24,12 @@ #include <errno.h> +/*! \brief Default websocket write timeout, in ms */ +#define AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT 100 + +/*! \brief Default websocket write timeout, in ms (as a string) */ +#define AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT_STR "100" + /*! * \file http_websocket.h * \brief Support for WebSocket connections within the Asterisk HTTP server and client @@ -324,4 +330,16 @@ AST_OPTIONAL_API(struct ast_websocket *, ast_websocket_client_create, */ AST_OPTIONAL_API(const char *, ast_websocket_client_accept_protocol, (struct ast_websocket *ws), { return NULL;}); + +/*! + * \brief Set the timeout on a non-blocking WebSocket session. + * + * \since 11.11.0 + * \since 12.4.0 + * + * \retval 0 on success + * \retval -1 on failure + */ +AST_OPTIONAL_API(int, ast_websocket_set_timeout, (struct ast_websocket *session, int timeout), {return -1;}); + #endif diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index c7e99aded4ed15e47b58d874406346b570b5e273..de654ee8dbd94c7b6cfc8aeea9fd60fb2eacab41 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -128,6 +128,8 @@ struct ast_sip_transport { unsigned int tos; /*! QOS COS value */ unsigned int cos; + /*! Write timeout */ + int write_timeout; }; /*! diff --git a/res/ari/ari_websockets.c b/res/ari/ari_websockets.c index 90d6f0fdb3f49483c444a39ae5452187ae758d53..ff0a53c4fc7b11e4354a33aa381b2a7b73ecef27 100644 --- a/res/ari/ari_websockets.c +++ b/res/ari/ari_websockets.c @@ -56,11 +56,16 @@ struct ast_ari_websocket_session *ast_ari_websocket_session_create( struct ast_websocket *ws_session, int (*validator)(struct ast_json *)) { RAII_VAR(struct ast_ari_websocket_session *, session, NULL, ao2_cleanup); + RAII_VAR(struct ast_ari_conf *, config, ast_ari_config_get(), ao2_cleanup); if (ws_session == NULL) { return NULL; } + if (config == NULL || config->general == NULL) { + return NULL; + } + if (validator == NULL) { validator = null_validator; } @@ -72,6 +77,11 @@ struct ast_ari_websocket_session *ast_ari_websocket_session_create( return NULL; } + if (ast_websocket_set_timeout(ws_session, config->general->write_timeout)) { + ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n", + config->general->write_timeout); + } + session = ao2_alloc(sizeof(*session), websocket_session_dtor); if (!session) { return NULL; diff --git a/res/ari/config.c b/res/ari/config.c index 59c4d7d9499c2a6a7e292a9b9bfad6b47ec6d37a..667d91ac07ae5ade18f55c095d4bc81ab3d97105 100644 --- a/res/ari/config.c +++ b/res/ari/config.c @@ -27,6 +27,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/config_options.h" +#include "asterisk/http_websocket.h" #include "internal.h" /*! \brief Locking container for safe configuration access. */ @@ -320,6 +321,9 @@ int ast_ari_config_init(void) aco_option_register(&cfg_info, "allowed_origins", ACO_EXACT, general_options, "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_ari_conf_general, allowed_origins)); + aco_option_register(&cfg_info, "websocket_write_timeout", ACO_EXACT, general_options, + AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT_STR, OPT_INT_T, PARSE_IN_RANGE, + FLDSET(struct ast_ari_conf_general, write_timeout), 1, INT_MAX); aco_option_register(&cfg_info, "type", ACO_EXACT, user, NULL, OPT_NOOP_T, 0, 0); diff --git a/res/ari/internal.h b/res/ari/internal.h index 8453747f19f094d7585f924f27aa55a9934277bb..93ea0b773c12bfc6626777ed6644b7cf6b8e194d 100644 --- a/res/ari/internal.h +++ b/res/ari/internal.h @@ -65,6 +65,8 @@ struct ast_ari_conf { struct ast_ari_conf_general { /*! Enabled by default, disabled if false. */ int enabled; + /*! Write timeout for websocket connections */ + int write_timeout; /*! Encoding format used during output (default compact). */ enum ast_json_encoding_format format; /*! Authentication realm */ diff --git a/res/res_ari.c b/res/res_ari.c index ce7027e44974664f2416594e8da47e5198e95911..acdbbfe9aa0fb80acdea95ccdf27f5f62883c4e1 100644 --- a/res/res_ari.c +++ b/res/res_ari.c @@ -95,6 +95,14 @@ <ref type="link">https://wiki.asterisk.org/wiki/display/AST/Asterisk+Builtin+mini-HTTP+Server</ref> </see-also> </configOption> + <configOption name="websocket_write_timeout"> + <synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis> + <description> + <para>If a websocket connection accepts input slowly, the timeout + for writes to it can be increased to keep it from being disconnected. + Value is in milliseconds; default is 100 ms.</para> + </description> + </configOption> <configOption name="pretty"> <synopsis>Responses from ARI are formatted to be human readable</synopsis> </configOption> diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c index 07fcd9e2ee66a68103c250be0d5c1f0d56b7818f..90744a1b3bb06ec1d9f3cd4c39fbd072a2fe7ad5 100644 --- a/res/res_http_websocket.c +++ b/res/res_http_websocket.c @@ -81,6 +81,7 @@ struct ast_websocket { size_t payload_len; /*!< Length of the payload */ char *payload; /*!< Pointer to the payload */ size_t reconstruct; /*!< Number of bytes before a reconstructed payload will be returned and a new one started */ + int timeout; /*!< The timeout for operations on the socket */ unsigned int secure:1; /*!< Bit to indicate that the transport is secure */ unsigned int closing:1; /*!< Bit to indicate that the session is in the process of being closed */ unsigned int close_sent:1; /*!< Bit to indicate that the session close opcode has been sent and no further data will be sent */ @@ -260,7 +261,7 @@ int AST_OPTIONAL_API_NAME(ast_websocket_close)(struct ast_websocket *session, ui session->close_sent = 1; ao2_lock(session); - res = (fwrite(frame, 1, 4, session->f) == 4) ? 0 : -1; + res = ast_careful_fwrite(session->f, session->fd, frame, 4, session->timeout); ao2_unlock(session); return res; } @@ -303,13 +304,12 @@ int AST_OPTIONAL_API_NAME(ast_websocket_write)(struct ast_websocket *session, en ao2_unlock(session); return -1; } - - if (fwrite(frame, 1, header_size, session->f) != header_size) { + if (ast_careful_fwrite(session->f, session->fd, frame, header_size, session->timeout)) { ao2_unlock(session); return -1; } - if (fwrite(payload, 1, actual_length, session->f) != actual_length) { + if (ast_careful_fwrite(session->f, session->fd, payload, actual_length, session->timeout)) { ao2_unlock(session); return -1; } @@ -371,6 +371,13 @@ int AST_OPTIONAL_API_NAME(ast_websocket_set_nonblock)(struct ast_websocket *sess return 0; } +int AST_OPTIONAL_API_NAME(ast_websocket_set_timeout)(struct ast_websocket *session, int timeout) +{ + session->timeout = timeout; + + return 0; +} + /* MAINTENANCE WARNING on ast_websocket_read()! * * We have to keep in mind during this function that the fact that session->fd seems ready @@ -514,8 +521,10 @@ int AST_OPTIONAL_API_NAME(ast_websocket_read)(struct ast_websocket *session, cha } /* Per the RFC for PING we need to send back an opcode with the application data as received */ - if (*opcode == AST_WEBSOCKET_OPCODE_PING) { - ast_websocket_write(session, AST_WEBSOCKET_OPCODE_PONG, *payload, *payload_len); + if ((*opcode == AST_WEBSOCKET_OPCODE_PING) && (ast_websocket_write(session, AST_WEBSOCKET_OPCODE_PONG, *payload, *payload_len))) { + *payload_len = 0; + ast_websocket_close(session, 1009); + return 0; } session->payload = new_payload; @@ -696,6 +705,7 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan ao2_ref(protocol_handler, -1); return 0; } + session->timeout = AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT; fprintf(ser->f, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: %s\r\n" diff --git a/res/res_pjsip.c b/res/res_pjsip.c index 2602660ee393d98c24c4c17b15cc54465ed79217..45b8e7e029445e3119401e66c42de04adce4c3aa 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -869,6 +869,14 @@ or the <replaceable>wss</replaceable> protocols.</para></note> </description> </configOption> + <configOption name="websocket_write_timeout"> + <synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis> + <description> + <para>If a websocket connection accepts input slowly, the timeout + for writes to it can be increased to keep it from being disconnected. + Value is in milliseconds; default is 100 ms.</para> + </description> + </configOption> </configObject> <configObject name="contact"> <synopsis>A way of creating an aliased name to a SIP URI</synopsis> diff --git a/res/res_pjsip/config_transport.c b/res/res_pjsip/config_transport.c index 22581ca52a275d6b4a7193e82cb5db2193f4373a..785fcc5ac57c7646d78e607c03b96c8e7210c871 100644 --- a/res/res_pjsip/config_transport.c +++ b/res/res_pjsip/config_transport.c @@ -28,6 +28,7 @@ #include "asterisk/sorcery.h" #include "asterisk/acl.h" #include "include/res_pjsip_private.h" +#include "asterisk/http_websocket.h" static int sip_transport_to_ami(const struct ast_sip_transport *transport, struct ast_str **buf) @@ -668,6 +669,7 @@ int ast_sip_initialize_sorcery_transport(void) ast_sorcery_object_field_register_custom(sorcery, "transport", "local_net", "", transport_localnet_handler, localnet_to_str, localnet_to_vl, 0, 0); ast_sorcery_object_field_register_custom(sorcery, "transport", "tos", "0", transport_tos_handler, tos_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sorcery, "transport", "cos", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_transport, cos)); + ast_sorcery_object_field_register(sorcery, "transport", "websocket_write_timeout", AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT_STR, OPT_INT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_transport, write_timeout), 1, INT_MAX); ast_sip_register_endpoint_formatter(&endpoint_transport_formatter); diff --git a/res/res_pjsip_transport_websocket.c b/res/res_pjsip_transport_websocket.c index 22962dab0d7957c0a1a96096eeccfeb1a0c3ca40..bae120a19c00f250a3583cfd3a06fb7f25a5c8eb 100644 --- a/res/res_pjsip_transport_websocket.c +++ b/res/res_pjsip_transport_websocket.c @@ -207,6 +207,37 @@ static int transport_read(void *data) return (read_data->payload_len == recvd) ? 0 : -1; } +static int get_write_timeout(void) +{ + int write_timeout = -1; + struct ao2_container *transports; + + transports = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "transport", AST_RETRIEVE_FLAG_ALL, NULL); + + if (transports) { + struct ao2_iterator it_transports = ao2_iterator_init(transports, 0); + struct ast_sip_transport *transport; + + for (; (transport = ao2_iterator_next(&it_transports)); ao2_cleanup(transport)) { + if (transport->type != AST_TRANSPORT_WS && transport->type != AST_TRANSPORT_WSS) { + continue; + } + ast_debug(5, "Found %s transport with write timeout: %d\n", + transport->type == AST_TRANSPORT_WS ? "WS" : "WSS", + transport->write_timeout); + write_timeout = MAX(write_timeout, transport->write_timeout); + } + ao2_cleanup(transports); + } + + if (write_timeout < 0) { + write_timeout = AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT; + } + + ast_debug(1, "Write timeout for WS/WSS transports: %d\n", write_timeout); + return write_timeout; +} + /*! \brief WebSocket connection handler. */ @@ -222,6 +253,11 @@ static void websocket_cb(struct ast_websocket *session, struct ast_variable *par return; } + if (ast_websocket_set_timeout(session, get_write_timeout())) { + ast_websocket_unref(session); + return; + } + if (!(serializer = ast_sip_create_serializer())) { ast_websocket_unref(session); return;