diff --git a/apps/app_stasis.c b/apps/app_stasis.c index bbe7718cc8..5809b06fd3 100644 --- a/apps/app_stasis.c +++ b/apps/app_stasis.c @@ -25,12 +25,14 @@ /*** MODULEINFO res_stasis + res_ari core ***/ #include "asterisk.h" #include "asterisk/app.h" +#include "asterisk/ari.h" #include "asterisk/module.h" #include "asterisk/pbx.h" #include "asterisk/stasis.h" @@ -86,6 +88,7 @@ static const char *stasis = "Stasis"; static int app_exec(struct ast_channel *chan, const char *data) { char *parse = NULL; + char *connection_id; int ret = -1; AST_DECLARE_APP_ARGS(args, @@ -104,13 +107,35 @@ static int app_exec(struct ast_channel *chan, const char *data) if (args.argc < 1) { ast_log(LOG_WARNING, "Stasis app_name argument missing\n"); - } else { - ret = stasis_app_exec(chan, - args.app_name, - args.argc - 1, - args.app_argv); + goto done; + } + + if (stasis_app_is_registered(args.app_name)) { + ast_debug(3, "%s: App '%s' is already registered\n", + ast_channel_name(chan), args.app_name); + ret = stasis_app_exec(chan, args.app_name, args.argc - 1, args.app_argv); + goto done; } + ast_debug(3, "%s: App '%s' is NOT already registered\n", + ast_channel_name(chan), args.app_name); + + /* + * The app isn't registered so we need to see if we have a + * per-call outbound websocket config we can use. + * connection_id will be freed by ast_ari_close_per_call_websocket(). + */ + connection_id = ast_ari_create_per_call_websocket(args.app_name, chan); + if (ast_strlen_zero(connection_id)) { + ast_log(LOG_WARNING, + "%s: Stasis app '%s' doesn't exist\n", + ast_channel_name(chan), args.app_name); + goto done; + } + + ret = stasis_app_exec(chan, connection_id, args.argc - 1, args.app_argv); + ast_ari_close_per_call_websocket(connection_id); +done: if (ret) { /* set ret to 0 so pbx_core doesnt hangup the channel */ if (!ast_check_hangup(chan)) { @@ -140,5 +165,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Stasis dialplan applicat .support_level = AST_MODULE_SUPPORT_CORE, .load = load_module, .unload = unload_module, - .requires = "res_stasis", + .requires = "res_stasis,res_ari", ); diff --git a/configs/samples/ari.conf.sample b/configs/samples/ari.conf.sample index 5ce3166bfa..be47297279 100644 --- a/configs/samples/ari.conf.sample +++ b/configs/samples/ari.conf.sample @@ -35,3 +35,66 @@ enabled = yes ; When set to no, ARI support is disabled. ; When set to plain, the password is in plaintext. ; ;password_format = plain + +; Outbound Websocket Connections +; +;[connection1] ; The connection name +;type = outbound_websocket ; Must be "outbound_websocket" +;connection_type = persistent : "persistent" or "per_call_config" + ; Default: none +;uri = ws://localhost:8765 ; The URI needed to contact the remote server + ; Default: none +;apps = app1, app2 ; A comma-separated list of Stasis applications + ; that will be served by this connection. + ; No other connection may serve these apps. + ; Default: none +;subscribe_all = no ; If set to "yes", the server will receive all + ; events just as though "subscribeAll=true" was + ; specified on an incoming websocket connection. + ; Default: no +;protocols = ari ; The websocket protocol expected by the server. + ; Default: none +;username = username ; An authentication username if required by the server. + ; Default: none +;password = password ; The authentication password for the username. + ; Default: none +;local_ari_user = local_user ; The name of a local ARI user defined above. + ; This controls whether this connection can make + ; read/write requests or is read-only. + ; Default: none +;connection_timeout = 500 ; Connection timeout in milliseconds. + ; Default: 500 +;reconnect_interval = 1000 ; Number of milliseconds between (re)connection + ; attempts. + ; Default: 500 +;reconnect_attempts = 4 ; The number of (re)connection attempts to make + ; before returning an error. For persistent + ; connections, set to -1 to retry indefinitely. + ; Default: 4 for per_call connections and -1 for + ; persistent connections. +;tls_enabled = no ; Set to "yes" to enable TLS connections. + ; Default: no +;ca_list_file = /etc/pki/tls/cert.pem + ; A file containing all CA certificates needed + ; for the connection. Not needed if your server + ; has a certificate from a recognized CA. + ; Default: none +;ca_list_path = /etc/pki/ca-trust/extracted/pem/directory-hash + ; A directory containing individual CA certificates + ; as an alternative to ca_list_file. Rarely needed. + ; Default: none +;cert_file = /etc/asterisk/cert.pem + ; If the server requires you to have a client + ; certificate, specify it here and if it wasn't + ; issued by a recognized CA, make sure the matching + ; CA certificate is available in ca_list_file or + ; ca_list_path. + ; Default: none +;priv_key_file = /etc/asterisk/privkey.pem + ; The private key for the client certificate. +;verify_server_cert = no ; Verify that the server certificate is valid. + ; Default: yes +;verify_server_hostname = no ; Verify that the hostname in the server's certificate + ; matches the hostname in the URI configured above. + ; Default: yes + diff --git a/include/asterisk/ari.h b/include/asterisk/ari.h index 165b6a867d..a3285c13f9 100644 --- a/include/asterisk/ari.h +++ b/include/asterisk/ari.h @@ -244,4 +244,51 @@ void ast_ari_response_created(struct ast_ari_response *response, */ void ast_ari_response_alloc_failed(struct ast_ari_response *response); +/*! + * \brief Create a per-call outbound websocket connection. + * + * \param app_name The app name. + * \param channel The channel to create the websocket for. + * + * This function should really only be called by app_stasis. + * + * A "per_call" websocket configuration must already exist in + * ari.conf that has 'app_name' in its 'apps' parameter. + * + * The channel uniqueid is used to create a unique app_id + * composed of "-" which will be + * returned from this call. This ID will be used to register + * an ephemeral Stasis application and should be used as the + * app_name for the call to stasis_app_exec(). When + * stasis_app_exec() returns, ast_ari_close_per_call_websocket() + * must be called with the app_id to close the websocket. + * + * The channel unique id is also used to detect when the + * StasisEnd event is sent for the channel. It's how + * ast_ari_close_per_call_websocket() knows that all + * messages for the channel have been sent and it's safe + * to close the websocket. + * + * \retval The ephemeral application id or NULL if one could + * not be created. This pointer will be freed by + * ast_ari_close_per_call_websocket(). Do not free + * it yourself. + */ +char *ast_ari_create_per_call_websocket(const char *app_name, + struct ast_channel *channel); + +/*! + * \brief Close a per-call outbound websocket connection. + * + * \param app_id The ephemeral application id returned by + * ast_ari_create_per_call_websocket(). + * + * This function should really only be called by app_stasis. + * + * \note This call will block until all messages for the + * channel have been sent or 5 seconds has elapsed. + * After that, the websocket will be closed. + */ +void ast_ari_close_per_call_websocket(char *app_id); + #endif /* _ASTERISK_ARI_H */ diff --git a/include/asterisk/vector.h b/include/asterisk/vector.h index 8276a7c497..e71db648b7 100644 --- a/include/asterisk/vector.h +++ b/include/asterisk/vector.h @@ -84,6 +84,17 @@ int ast_vector_string_split(struct ast_vector_string *dest, const char *input, const char *delim, int flags, int (*excludes_cmp)(const char *s1, const char *s2)); +/*! + * \brief Join the elements of a string vector into a single string. + * + * \param vec Pointer to the vector. + * \param delim String to separate elements with. + * + * \retval Resulting string. Must be freed with ast_free. + * + */ +char *ast_vector_string_join(struct ast_vector_string *vec, const char *delim); + /*! * \brief Define a vector structure with a read/write lock * diff --git a/main/strings.c b/main/strings.c index f370466776..a524d55025 100644 --- a/main/strings.c +++ b/main/strings.c @@ -403,6 +403,25 @@ char *ast_read_line_from_buffer(char **buffer) return start; } +char *ast_vector_string_join(struct ast_vector_string *vec, const char *delim) +{ + struct ast_str *buf = ast_str_create(256); + char *rtn; + int i; + + if (!buf) { + return NULL; + } + + for (i = 0; i < AST_VECTOR_SIZE(vec); i++) { + ast_str_append(&buf, 0, "%s%s", AST_VECTOR_GET(vec, i), delim); + } + ast_str_truncate(buf, -strlen(delim)); + rtn = ast_strdup(ast_str_buffer(buf)); + ast_free(buf); + return rtn; +} + int ast_vector_string_split(struct ast_vector_string *dest, const char *input, const char *delim, int flags, int (*excludes_cmp)(const char *s1, const char *s2)) diff --git a/res/ari/ari_doc.xml b/res/ari/ari_doc.xml new file mode 100644 index 0000000000..c31a00ffe9 --- /dev/null +++ b/res/ari/ari_doc.xml @@ -0,0 +1,283 @@ + + + + + HTTP binding for the Stasis API + + + + 12.0.0 + + General configuration settings + + + 12.0.0 + + Enable/disable the ARI module + + This option enables or disables the ARI module. + + ARI uses Asterisk's HTTP server, which must also be enabled in http.conf. + + + + http.conf + https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/ + + + + + 11.11.0 + 12.4.0 + + The timeout (in milliseconds) to set on WebSocket connections. + + 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. + + + + + 12.0.0 + + Responses from ARI are formatted to be human readable + + + + 12.0.0 + + Realm to use for authentication. Defaults to Asterisk REST Interface. + + + + 12.0.0 + + Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins. + + + + 14.2.0 + + Comma separated list of channel variables to display in channel json. + + + + + + 12.0.0 + + Per-user configuration settings + + + 13.30.0 + 16.7.0 + 17.1.0 + + Define this configuration section as a user. + + + Configure this section as a user + + + + + + 13.30.0 + 16.7.0 + 17.1.0 + + When set to yes, user is only authorized for read-only requests + + + + 13.30.0 + 16.7.0 + 17.1.0 + + Crypted or plaintext password (see password_format) + + + + 12.0.0 + + 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 using mkpasswd -m sha-512. When set to plain, the password is in plaintext + + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Outbound websocket configuration + + + 20.14.0 + 21.9.0 + 22.4.0 + + Must be "outbound_websocket". + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Full URI to remote server. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Comma separated list of protocols acceptable to the server. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Comma separated list of stasis applications that will use this websocket. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Server authentication username if required. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Server authentication password if required. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + The local ARI user to act as. + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Single persistent connection or per-call configuration. + + + Single persistent connection for all calls. + New connection for each call to the Stasis() dialplan app. + + + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Subscribe applications to all event + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Connection timeout (ms). + + + + 20.14.0 + 21.9.0 + 22.4.0 + + On failure, how many times should reconnection be attempted? + + If 0, no reconnection will be attempted and + an error will be returned immediately on failure. If a negative + number, reconnection will be attempted forever. Any other positive value + will attempt reconnection the specified number of times before returning + an error. + + + + + + 20.14.0 + 21.9.0 + 22.4.0 + + How often should reconnection be attempted (ms)? + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Enable TLS + + + + 20.14.0 + 21.9.0 + 22.4.0 + + File containing the server's CA certificate. (optional) + + + + 20.14.0 + 21.9.0 + 22.4.0 + + Path to a directory containing one or more hashed CA certificates. (optional) + + + + 20.14.0 + 21.9.0 + 22.4.0 + + File containing a client certificate. (optional) + + + + 20.14.0 + 21.9.0 + 22.4.0 + + File containing the client's private key. (optional) + + + + 20.14.0 + 21.9.0 + 22.4.0 + + If set to true, verify the server's certificate. (optional) + + + + 20.14.0 + 21.9.0 + 22.4.0 + + If set to true, verify that the server's hostname matches the common name in it's certificate. (optional) + + + + + \ No newline at end of file diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c index a1dda282b8..5ef3821112 100644 --- a/res/ari/ari_model_validators.c +++ b/res/ari/ari_model_validators.c @@ -2610,6 +2610,85 @@ ari_validator ast_ari_validate_application_move_failed_fn(void) return ast_ari_validate_application_move_failed; } +int ast_ari_validate_application_registered(struct ast_json *json) +{ + int res = 1; + struct ast_json_iter *iter; + int has_type = 0; + int has_application = 0; + int has_timestamp = 0; + + for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { + if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered field asterisk_id failed validation\n"); + res = 0; + } + } else + if (strcmp("type", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_type = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered field type failed validation\n"); + res = 0; + } + } else + if (strcmp("application", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_application = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered field application failed validation\n"); + res = 0; + } + } else + if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_timestamp = 1; + prop_is_valid = ast_ari_validate_date( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered field timestamp failed validation\n"); + res = 0; + } + } else + { + ast_log(LOG_ERROR, + "ARI ApplicationRegistered has undocumented field %s\n", + ast_json_object_iter_key(iter)); + res = 0; + } + } + + if (!has_type) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field type\n"); + res = 0; + } + + if (!has_application) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field application\n"); + res = 0; + } + + if (!has_timestamp) { + ast_log(LOG_ERROR, "ARI ApplicationRegistered missing required field timestamp\n"); + res = 0; + } + + return res; +} + +ari_validator ast_ari_validate_application_registered_fn(void) +{ + return ast_ari_validate_application_registered; +} + int ast_ari_validate_application_replaced(struct ast_json *json) { int res = 1; @@ -2689,6 +2768,85 @@ ari_validator ast_ari_validate_application_replaced_fn(void) return ast_ari_validate_application_replaced; } +int ast_ari_validate_application_unregistered(struct ast_json *json) +{ + int res = 1; + struct ast_json_iter *iter; + int has_type = 0; + int has_application = 0; + int has_timestamp = 0; + + for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { + if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered field asterisk_id failed validation\n"); + res = 0; + } + } else + if (strcmp("type", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_type = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered field type failed validation\n"); + res = 0; + } + } else + if (strcmp("application", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_application = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered field application failed validation\n"); + res = 0; + } + } else + if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_timestamp = 1; + prop_is_valid = ast_ari_validate_date( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered field timestamp failed validation\n"); + res = 0; + } + } else + { + ast_log(LOG_ERROR, + "ARI ApplicationUnregistered has undocumented field %s\n", + ast_json_object_iter_key(iter)); + res = 0; + } + } + + if (!has_type) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field type\n"); + res = 0; + } + + if (!has_application) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field application\n"); + res = 0; + } + + if (!has_timestamp) { + ast_log(LOG_ERROR, "ARI ApplicationUnregistered missing required field timestamp\n"); + res = 0; + } + + return res; +} + +ari_validator ast_ari_validate_application_unregistered_fn(void) +{ + return ast_ari_validate_application_unregistered; +} + int ast_ari_validate_bridge_attended_transfer(struct ast_json *json) { int res = 1; @@ -6085,9 +6243,15 @@ int ast_ari_validate_event(struct ast_json *json) if (strcmp("ApplicationMoveFailed", discriminator) == 0) { return ast_ari_validate_application_move_failed(json); } else + if (strcmp("ApplicationRegistered", discriminator) == 0) { + return ast_ari_validate_application_registered(json); + } else if (strcmp("ApplicationReplaced", discriminator) == 0) { return ast_ari_validate_application_replaced(json); } else + if (strcmp("ApplicationUnregistered", discriminator) == 0) { + return ast_ari_validate_application_unregistered(json); + } else if (strcmp("BridgeAttendedTransfer", discriminator) == 0) { return ast_ari_validate_bridge_attended_transfer(json); } else @@ -6301,9 +6465,15 @@ int ast_ari_validate_message(struct ast_json *json) if (strcmp("ApplicationMoveFailed", discriminator) == 0) { return ast_ari_validate_application_move_failed(json); } else + if (strcmp("ApplicationRegistered", discriminator) == 0) { + return ast_ari_validate_application_registered(json); + } else if (strcmp("ApplicationReplaced", discriminator) == 0) { return ast_ari_validate_application_replaced(json); } else + if (strcmp("ApplicationUnregistered", discriminator) == 0) { + return ast_ari_validate_application_unregistered(json); + } else if (strcmp("BridgeAttendedTransfer", discriminator) == 0) { return ast_ari_validate_bridge_attended_transfer(json); } else diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h index a351b44304..58701c2af1 100644 --- a/res/ari/ari_model_validators.h +++ b/res/ari/ari_model_validators.h @@ -603,6 +603,22 @@ int ast_ari_validate_application_move_failed(struct ast_json *json); */ ari_validator ast_ari_validate_application_move_failed_fn(void); +/*! + * \brief Validator for ApplicationRegistered. + * + * Notification that a Stasis app has been registered. + * + * \param json JSON object to validate. + * \retval True (non-zero) if valid. + * \retval False (zero) if invalid. + */ +int ast_ari_validate_application_registered(struct ast_json *json); + +/*! + * \brief Function pointer to ast_ari_validate_application_registered(). + */ +ari_validator ast_ari_validate_application_registered_fn(void); + /*! * \brief Validator for ApplicationReplaced. * @@ -621,6 +637,22 @@ int ast_ari_validate_application_replaced(struct ast_json *json); */ ari_validator ast_ari_validate_application_replaced_fn(void); +/*! + * \brief Validator for ApplicationUnregistered. + * + * Notification that a Stasis app has been unregistered. + * + * \param json JSON object to validate. + * \retval True (non-zero) if valid. + * \retval False (zero) if invalid. + */ +int ast_ari_validate_application_unregistered(struct ast_json *json); + +/*! + * \brief Function pointer to ast_ari_validate_application_unregistered(). + */ +ari_validator ast_ari_validate_application_unregistered_fn(void); + /*! * \brief Validator for BridgeAttendedTransfer. * @@ -1612,11 +1644,21 @@ ari_validator ast_ari_validate_application_fn(void); * - args: List[string] (required) * - channel: Channel (required) * - destination: string (required) + * ApplicationRegistered + * - asterisk_id: string + * - type: string (required) + * - application: string (required) + * - timestamp: Date (required) * ApplicationReplaced * - asterisk_id: string * - type: string (required) * - application: string (required) * - timestamp: Date (required) + * ApplicationUnregistered + * - asterisk_id: string + * - type: string (required) + * - application: string (required) + * - timestamp: Date (required) * BridgeAttendedTransfer * - asterisk_id: string * - type: string (required) diff --git a/res/ari/ari_websockets.c b/res/ari/ari_websockets.c index 3712dd7d10..005f986af3 100644 --- a/res/ari/ari_websockets.c +++ b/res/ari/ari_websockets.c @@ -21,15 +21,17 @@ #include "resource_events.h" #include "ari_websockets.h" #include "internal.h" -#if defined(AST_DEVMODE) #include "ari_model_validators.h" -#endif #include "asterisk/app.h" #include "asterisk/ari.h" #include "asterisk/astobj2.h" #include "asterisk/http_websocket.h" #include "asterisk/module.h" +#include "asterisk/pbx.h" #include "asterisk/stasis_app.h" +#include "asterisk/time.h" +#include "asterisk/uuid.h" +#include "asterisk/vector.h" /*! \file @@ -38,30 +40,36 @@ * \author David M. Lee, II */ -/*! Number of buckets for the event session registry. Remember to keep it a prime number! */ -#define ARI_WS_SESSION_NUM_BUCKETS 23 +/*! Number of buckets for the ari_ws_session registry. Remember to keep it a prime number! */ +#define SESSION_REGISTRY_NUM_BUCKETS 23 -/*! Number of buckets for a websocket apps container. Remember to keep it a prime number! */ -#define APPS_NUM_BUCKETS 7 +/*! Initial size of websocket session apps vector */ +#define APPS_INIT_SIZE 7 -/*! Initial size of a message queue. */ +/*! Initial size of the websocket session message queue. */ #define MESSAGES_INIT_SIZE 23 +#define ARI_CONTEXT_REGISTRAR "res_ari" -/*! \brief Local registry for created \ref event_session objects. */ -static struct ao2_container *ari_ws_session_registry; +/*! \brief Local registry for created \ref ari_ws_session objects. */ +static struct ao2_container *session_registry; struct ast_websocket_server *ast_ws_server; -#define MAX_VALS 128 +#if defined(AST_DEVMODE) + ari_validator ari_validate_message_fn = ast_ari_validate_message; +#else + /*! + * \brief Validator that always succeeds. + */ + static int null_validator(struct ast_json *json) + { + return 1; + } + + ari_validator ari_validate_message_fn = null_validator; +#endif -/*! - * \brief Validator that always succeeds. - */ -static int null_validator(struct ast_json *json) -{ - return 1; -} #define VALIDATION_FAILED \ "{" \ @@ -69,16 +77,18 @@ static int null_validator(struct ast_json *json) " \"message\": \"Message validation failed\"" \ "}" -static int ari_ws_session_write( - struct ari_ws_session *ari_ws_session, - struct ast_json *message) +static int session_write(struct ari_ws_session *session, struct ast_json *message) { RAII_VAR(char *, str, NULL, ast_json_free); + if (!session || !session->ast_ws_session || !message) { + return -1; + } + #ifdef AST_DEVMODE - if (!ari_ws_session->validator(message)) { + if (!session->validator(message)) { ast_log(LOG_ERROR, "Outgoing message failed validation\n"); - return ast_websocket_write_string(ari_ws_session->ast_ws_session, VALIDATION_FAILED); + return ast_websocket_write_string(session->ast_ws_session, VALIDATION_FAILED); } #endif @@ -89,73 +99,108 @@ static int ari_ws_session_write( return -1; } - if (ast_websocket_write_string(ari_ws_session->ast_ws_session, str)) { + if (ast_websocket_write_string(session->ast_ws_session, str)) { ast_log(LOG_NOTICE, "Problem occurred during websocket write to %s, websocket closed\n", - ast_sockaddr_stringify(ast_websocket_remote_address(ari_ws_session->ast_ws_session))); + ast_sockaddr_stringify(ast_websocket_remote_address(session->ast_ws_session))); return -1; } return 0; } -/*! - * \internal - * \brief Updates the websocket session. - * - * \details If the value of the \c ws_session is not \c NULL and there are messages in the - * event session's \c message_queue, the messages are dispatched and removed from - * the queue. - * - * \param ari_ws_session The ARI websocket session - * \param ast_ws_session The Asterisk websocket session - */ -static int ari_ws_session_update( - struct ari_ws_session *ari_ws_session, - struct ast_websocket *ast_ws_session) +static void session_send_or_queue(struct ari_ws_session *session, + struct ast_json *message, const char *msg_type, const char *app_name, + int debug_app) { - RAII_VAR(struct ast_ari_conf *, config, ast_ari_config_get(), ao2_cleanup); - int i; + const char *msg_timestamp, *msg_ast_id; - if (ast_ws_session == NULL) { - return -1; + msg_timestamp = S_OR( + ast_json_string_get(ast_json_object_get(message, "timestamp")), ""); + if (ast_strlen_zero(msg_timestamp)) { + if (ast_json_object_set(message, "timestamp", ast_json_timeval(ast_tvnow(), NULL))) { + ast_log(LOG_ERROR, + "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n", + session->remote_addr, msg_type, app_name); + return; + } } - if (config == NULL || config->general == NULL) { - return -1; - } + msg_ast_id = S_OR( + ast_json_string_get(ast_json_object_get(message, "asterisk_id")), ""); + if (ast_strlen_zero(msg_ast_id)) { + char eid[20]; - if (ast_websocket_set_nonblock(ast_ws_session) != 0) { - ast_log(LOG_ERROR, - "ARI web socket failed to set nonblock; closing: %s\n", - strerror(errno)); - return -1; + if (ast_json_object_set(message, "asterisk_id", + ast_json_string_create(ast_eid_to_str(eid, sizeof(eid), &ast_eid_default)))) { + ao2_unlock(session); + ast_log(LOG_ERROR, + "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n", + session->remote_addr, msg_type, app_name); + } } - if (ast_websocket_set_timeout(ast_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); - } + if (!session->ast_ws_session) { + /* If the websocket is NULL, the message goes to the queue */ + if (AST_VECTOR_APPEND(&session->message_queue, message) == 0) { + ast_json_ref(message); + } + /* + * If the msg_type one of the Application* types, the websocket + * might not be there yet so don't log. + */ + if (!ast_begins_with(msg_type, "Application")) { + ast_log(LOG_WARNING, + "%s: Queued '%s' message for Stasis app '%s'; websocket is not ready\n", + session->remote_addr, + msg_type, + app_name); + } + } else { - ao2_ref(ast_ws_session, +1); - ari_ws_session->ast_ws_session = ast_ws_session; - ao2_lock(ari_ws_session); - for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->message_queue); i++) { - struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, i); - ari_ws_session_write(ari_ws_session, msg); - ast_json_unref(msg); + if (DEBUG_ATLEAST(4) || debug_app) { + char *str = ast_json_dump_string_format(message, AST_JSON_PRETTY); + + ast_verbose("<--- Sending ARI event to %s --->\n%s\n", + session->remote_addr, + str); + ast_json_free(str); + } + session_write(session, message); } +} - AST_VECTOR_RESET(&ari_ws_session->message_queue, AST_VECTOR_ELEM_CLEANUP_NOOP); - ao2_unlock(ari_ws_session); +static void session_send_app_event(struct ari_ws_session *session, + const char *event_type, const char *app_name) +{ + char eid[20]; + int debug_app = stasis_app_get_debug_by_name(app_name); + struct ast_json *msg = ast_json_pack("{s:s, s:o?, s:s, s:s }", + "type", event_type, + "timestamp", ast_json_timeval(ast_tvnow(), NULL), + "application", app_name, + "asterisk_id", ast_eid_to_str(eid, sizeof(eid), &ast_eid_default)); - return 0; + if (!msg) { + return; + } + ast_debug(3, "%s: Sending '%s' event to app '%s'\n", session->session_id, + event_type, app_name); + /* + * We don't want to use ari_websocket_send_event() here because + * the app may be unregistered which will cause stasis_app_event_allowed + * to return false. + */ + session_send_or_queue(session, msg, event_type, app_name, debug_app); + ast_json_unref(msg); } -static struct ast_json *ari_ws_session_read( - struct ari_ws_session *ari_ws_session) +static struct ast_json *session_read(struct ari_ws_session *session) { RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); - if (ast_websocket_fd(ari_ws_session->ast_ws_session) < 0) { + if (!session || !session->ast_ws_session) { + return NULL; + } + if (ast_websocket_fd(session->ast_ws_session) < 0) { return NULL; } @@ -167,7 +212,7 @@ static struct ast_json *ari_ws_session_read( int fragmented; res = ast_wait_for_input( - ast_websocket_fd(ari_ws_session->ast_ws_session), -1); + ast_websocket_fd(session->ast_ws_session), -1); if (res <= 0) { ast_log(LOG_WARNING, "WebSocket poll error: %s\n", @@ -175,7 +220,7 @@ static struct ast_json *ari_ws_session_read( return NULL; } - res = ast_websocket_read(ari_ws_session->ast_ws_session, &payload, + res = ast_websocket_read(session->ast_ws_session, &payload, &payload_len, &opcode, &fragmented); if (res != 0) { @@ -200,7 +245,7 @@ static struct ast_json *ari_ws_session_read( "reason_phrase", "Failed to parse request message JSON", "uri", "" ); - ari_websocket_send_event(ari_ws_session, ari_ws_session->app_name, + ari_websocket_send_event(session, session->app_name, error, 0); ast_json_unref(error); ast_log(LOG_WARNING, @@ -241,88 +286,58 @@ void ari_handle_websocket( * \param message The dispatched message. * \param debug_app Debug flag for the application. */ -void ari_websocket_send_event(struct ari_ws_session *ari_ws_session, +void ari_websocket_send_event(struct ari_ws_session *session, const char *app_name, struct ast_json *message, int debug_app) { - char *remote_addr = ast_sockaddr_stringify( - ast_websocket_remote_address(ari_ws_session->ast_ws_session)); - const char *msg_type, *msg_application, *msg_timestamp, *msg_ast_id; + char *remote_addr = session->ast_ws_session ? ast_sockaddr_stringify( + ast_websocket_remote_address(session->ast_ws_session)) : ""; + const char *msg_type, *msg_application; SCOPE_ENTER(4, "%s: Dispatching message from Stasis app '%s'\n", remote_addr, app_name); - ast_assert(ari_ws_session != NULL); + ast_assert(session != NULL); - ao2_lock(ari_ws_session); + ao2_lock(session); msg_type = S_OR(ast_json_string_get(ast_json_object_get(message, "type")), ""); msg_application = S_OR( - ast_json_string_get(ast_json_object_get(message, "application")), ""); + ast_json_string_get(ast_json_object_get(message, "application")), app_name); /* If we've been replaced, remove the application from our local websocket_apps container */ - if (strcmp(msg_type, "ApplicationReplaced") == 0 && + if (session->type == ARI_WS_TYPE_INBOUND + && strcmp(msg_type, "ApplicationReplaced") == 0 && strcmp(msg_application, app_name) == 0) { - ao2_find(ari_ws_session->websocket_apps, msg_application, - OBJ_UNLINK | OBJ_NODATA); - } - - msg_timestamp = S_OR( - ast_json_string_get(ast_json_object_get(message, "timestamp")), ""); - if (ast_strlen_zero(msg_timestamp)) { - if (ast_json_object_set(message, "timestamp", ast_json_timeval(ast_tvnow(), NULL))) { - ao2_unlock(ari_ws_session); - SCOPE_EXIT_LOG_RTN(LOG_WARNING, - "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n", - remote_addr, msg_type, msg_application); - } - } - - msg_ast_id = S_OR( - ast_json_string_get(ast_json_object_get(message, "asterisk_id")), ""); - if (ast_strlen_zero(msg_ast_id)) { - char eid[20]; - - if (ast_json_object_set(message, "asterisk_id", - ast_json_string_create(ast_eid_to_str(eid, sizeof(eid), &ast_eid_default)))) { - ao2_unlock(ari_ws_session); - SCOPE_EXIT_LOG_RTN(LOG_WARNING, - "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n", - remote_addr, msg_type, msg_application); - } + AST_VECTOR_REMOVE_CMP_ORDERED(&session->websocket_apps, + app_name, ast_strings_equal, ast_free_ptr); } /* Now, we need to determine our state to see how we will handle the message */ if (ast_json_object_set(message, "application", ast_json_string_create(app_name))) { - ao2_unlock(ari_ws_session); + ao2_unlock(session); SCOPE_EXIT_LOG_RTN(LOG_WARNING, "%s: Failed to dispatch '%s' message from Stasis app '%s'; could not update message\n", remote_addr, msg_type, msg_application); } - if (!ari_ws_session) { - /* If the websocket is NULL, the message goes to the queue */ - if (!AST_VECTOR_APPEND(&ari_ws_session->message_queue, message)) { - ast_json_ref(message); - } - ast_log(LOG_WARNING, - "%s: Queued '%s' message for Stasis app '%s'; websocket is not ready\n", - remote_addr, - msg_type, - msg_application); - } else if (stasis_app_event_allowed(app_name, message)) { - - if (TRACE_ATLEAST(4) || debug_app) { - char *str = ast_json_dump_string_format(message, AST_JSON_PRETTY); + if (stasis_app_event_allowed(app_name, message)) { + session_send_or_queue(session, message, msg_type, + app_name, debug_app); + } - ast_verbose("<--- Sending ARI event to %s --->\n%s\n", - remote_addr, - str); - ast_json_free(str); + if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL + && !ast_strlen_zero(session->channel_id) + && ast_strings_equal(msg_type, "StasisEnd")) { + struct ast_json *chan = ast_json_object_get(message, "channel"); + struct ast_json *id_obj = ast_json_object_get(chan, "id"); + const char *id = ast_json_string_get(id_obj); + if (!ast_strlen_zero(id) + && ast_strings_equal(id, session->channel_id)) { + ast_debug(3, "%s: StasisEnd message sent for channel '%s'\n", + remote_addr, id); + session->stasis_end_sent = 1; } - - ari_ws_session_write(ari_ws_session, message); } - - ao2_unlock(ari_ws_session); + ao2_unlock(session); SCOPE_EXIT("%s: Dispatched '%s' message from Stasis app '%s'\n", remote_addr, msg_type, app_name); } @@ -331,69 +346,169 @@ static void stasis_app_message_handler(void *data, const char *app_name, struct ast_json *message) { int debug_app = stasis_app_get_debug_by_name(app_name); - struct ari_ws_session *ari_ws_session = data; - ast_assert(ari_ws_session != NULL); - ari_websocket_send_event(ari_ws_session, app_name, message, debug_app); + struct ari_ws_session *session = data; + + if (!session) { + ast_debug(3, "Stasis app '%s' message handler called with NULL session. OK for per_call_config websocket.\n", + app_name); + return; + } + + ari_websocket_send_event(session, app_name, message, debug_app); } -static int parse_app_args(struct ast_variable *get_params, - struct ast_ari_response * response, - struct ast_ari_events_event_websocket_args *args) +static void session_unref(struct ari_ws_session *session) { - struct ast_variable *i; - RAII_VAR(char *, app_parse, NULL, ast_free); + if (!session) { + return; + } + ast_debug(4, "%s: Unreffing ARI websocket session\n", session->session_id); + ao2_ref(session, -1); +} - for (i = get_params; i; i = i->next) { - if (strcmp(i->name, "app") == 0) { - /* Parse comma separated list */ - char *vals[MAX_VALS]; - size_t j; +static void session_unregister_app_cb(char *app_name, struct ari_ws_session *session) +{ + ast_debug(3, "%s: Trying to unregister app '%s'\n", + session->session_id, app_name); + if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + char context_name[AST_MAX_CONTEXT + 1]; + sprintf(context_name, "%s%s", STASIS_CONTEXT_PREFIX, app_name); + ast_debug(3, "%s: Unregistering context '%s' for app '%s'\n", + session->session_id, context_name, app_name); + ast_context_destroy_by_name(context_name, ARI_CONTEXT_REGISTRAR); + } else { + ast_debug(3, "%s: Unregistering stasis app '%s' and unsubscribing from all events.\n", + session->session_id, app_name); + stasis_app_unregister(app_name); + } - app_parse = ast_strdup(i->value); - if (!app_parse) { - ast_ari_response_alloc_failed(response); - return -1; - } + /* + * We don't send ApplicationUnregistered events for outbound per-call + * configs because there's no websocket to send them via or to + * inbound websockets because the websocket is probably closed already. + */ + if (!(session->type + & (ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG | ARI_WS_TYPE_INBOUND))) { + session_send_app_event(session, "ApplicationUnregistered", app_name); + } +} + +static void session_unregister_apps(struct ari_ws_session *session) +{ + int app_count = (int)AST_VECTOR_SIZE(&session->websocket_apps); + + if (app_count == 0) { + return; + } + ast_debug(3, "%s: Unregistering stasis apps.\n", session->session_id); + + AST_VECTOR_CALLBACK_VOID(&session->websocket_apps, session_unregister_app_cb, + session); + AST_VECTOR_RESET(&session->websocket_apps, ast_free_ptr); + + return; +} + +static int session_register_apps(struct ari_ws_session *session, + const char *_apps, int subscribe_all) +{ + char *apps = ast_strdupa(_apps); + char *app_name; + int app_counter = 0; + + ast_debug(3, "%s: Registering apps '%s'. Subscribe all: %s\n", + session->session_id, apps, subscribe_all ? "yes" : "no"); + + while ((app_name = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) { + + if (ast_strlen_zero(app_name)) { + ast_log(LOG_WARNING, "%s: Invalid application name\n", session->session_id); + return -1; + } - if (strlen(app_parse) == 0) { - /* ast_app_separate_args can't handle "" */ - args->app_count = 1; - vals[0] = app_parse; + if (strlen(app_name) > ARI_MAX_APP_NAME_LEN) { + ast_log(LOG_WARNING, "%s: Websocket app '%s' > %d characters\n", + session->session_id, app_name, (int)ARI_MAX_APP_NAME_LEN); + return -1; + } + + if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + /* + * Outbound per-call configs only create a dialplan context. + * If they registered stasis apps there'd be no way for the + * Stasis dialplan app to know that it needs to start a + * per-call websocket connection. + */ + char context_name[AST_MAX_CONTEXT + 1]; + + sprintf(context_name, "%s%s", STASIS_CONTEXT_PREFIX, app_name); + if (!ast_context_find(context_name)) { + if (!ast_context_find_or_create(NULL, NULL, context_name, + ARI_CONTEXT_REGISTRAR)) { + ast_log(LOG_WARNING, "%s: Could not create context '%s'\n", + session->session_id, context_name); + return -1; + } else { + ast_add_extension(context_name, 0, "_.", 1, NULL, NULL, + "Stasis", ast_strdup(app_name), ast_free_ptr, + ARI_CONTEXT_REGISTRAR); + ast_add_extension(context_name, 0, "h", 1, NULL, NULL, + "NoOp", NULL, NULL, ARI_CONTEXT_REGISTRAR); + } } else { - args->app_count = ast_app_separate_args( - app_parse, ',', vals, - ARRAY_LEN(vals)); + ast_debug(3, "%s: Context '%s' already exists\n", session->session_id, + context_name); } + } else { + int already_registered = stasis_app_is_registered(app_name); + int res = 0; - if (args->app_count == 0) { - ast_ari_response_alloc_failed(response); - return -1; + if (subscribe_all) { + res = stasis_app_register_all(app_name, stasis_app_message_handler, + session); + } else { + res = stasis_app_register(app_name, stasis_app_message_handler, + session); } - if (args->app_count >= MAX_VALS) { - ast_ari_response_error(response, 400, - "Bad Request", - "Too many values for app"); + if (res != 0) { return -1; } - args->app = ast_malloc(sizeof(*args->app) * args->app_count); - if (!args->app) { - ast_ari_response_alloc_failed(response); - return -1; + /* + * If there was an existing app by the same name, the register handler + * will have sent an ApplicationReplaced event. If it's a new app, we + * send an ApplicationRegistered event. + * + * Except... There's no websocket to send it on for outbound per-call + * configs and inbound websockets don't need them because they aready + * know what apps they've registered for. + */ + if (!already_registered + && !(session->type & (ARI_WS_TYPE_INBOUND | ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG))) { + session_send_app_event(session, "ApplicationRegistered", + app_name); } + } - for (j = 0; j < args->app_count; ++j) { - args->app[j] = (vals[j]); + if (AST_VECTOR_ADD_SORTED(&session->websocket_apps, ast_strdup(app_name), strcmp)) { + ast_log(LOG_WARNING, "%s: Unable to add app '%s' to apps container\n", + session->session_id, app_name); + return -1; + } + + app_counter++; + if (app_counter == 1) { + ast_free(session->app_name); + session->app_name = ast_strdup(app_name); + if (!session->app_name) { + ast_log(LOG_WARNING, "%s: Unable to duplicate app name\n", + session->session_id); + return -1; } - } else if (strcmp(i->name, "subscribeAll") == 0) { - args->subscribe_all = ast_true(i->value); } } - args->app_parse = app_parse; - app_parse = NULL; - return 0; } @@ -410,32 +525,25 @@ static int parse_app_args(struct ast_variable *get_params, * \internal * \brief Reset the ari_ws_session without destroying it. * It can't be reused and will be cleaned up by the caller. + * This should only be called by session_create() + * and session_cleanup(). */ -static void ari_ws_session_reset(struct ari_ws_session *ari_ws_session) +static void session_reset(struct ari_ws_session *session) { - struct ao2_iterator i; - char *app; - int j; - SCOPED_AO2LOCK(lock, ari_ws_session); + SCOPED_AO2LOCK(lock, session); + + ast_debug(3, "%s: Resetting ARI websocket session\n", + session->session_id); /* Clean up the websocket_apps container */ - if (ari_ws_session->websocket_apps) { - i = ao2_iterator_init(ari_ws_session->websocket_apps, 0); - while ((app = ao2_iterator_next(&i))) { - stasis_app_unregister(app); - ao2_cleanup(app); - } - ao2_iterator_destroy(&i); - ao2_cleanup(ari_ws_session->websocket_apps); - ari_ws_session->websocket_apps = NULL; + if (AST_VECTOR_SIZE(&session->websocket_apps) > 0) { + session_unregister_apps(session); } + AST_VECTOR_RESET(&session->websocket_apps, ast_free_ptr); + AST_VECTOR_FREE(&session->websocket_apps); - /* Clean up the message_queue container */ - for (j = 0; j < AST_VECTOR_SIZE(&ari_ws_session->message_queue); j++) { - struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, j); - ast_json_unref(msg); - } - AST_VECTOR_FREE(&ari_ws_session->message_queue); + AST_VECTOR_RESET(&session->message_queue, ast_json_unref); + AST_VECTOR_FREE(&session->message_queue); } /*! @@ -444,208 +552,255 @@ static void ari_ws_session_reset(struct ari_ws_session *ari_ws_session) * This unlinks the ari_ws_session from the registry and cleans up the * decrements the reference count. */ -static void ari_ws_session_cleanup(struct ari_ws_session *ari_ws_session) +static void session_cleanup(struct ari_ws_session *session) { - if (!ari_ws_session) { + if (!session) { return; } + ast_debug(3, "%s: Cleaning up ARI websocket session RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + + session_reset(session); + + if (session_registry) { + ast_debug(3, "%s: Unlinking websocket session from registry RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + ao2_unlink(session_registry, session); + } - ari_ws_session_reset(ari_ws_session); - if (ari_ws_session_registry) { - ao2_unlink(ari_ws_session_registry, ari_ws_session); + /* + * If this is a per-call config then its only reference + * was held by the registry container so we don't need + * to unref it here. + */ + if (session->type != ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + session_unref(session); } - ao2_ref(ari_ws_session, -1); } /*! * \internal * \brief The ao2 destructor. - * This cleans up the reference to the parent ast_websocket. + * This cleans up the reference to the parent ast_websocket and the + * outbound connection websocket if any. */ -static void ari_ws_session_dtor(void *obj) +static void session_dtor(void *obj) { - struct ari_ws_session *ari_ws_session = obj; - - ast_free(ari_ws_session->app_name); - if (!ari_ws_session->ast_ws_session) { + struct ari_ws_session *session = obj; + + ast_debug(3, "%s: Destroying ARI websocket session\n", + session->session_id); + + ast_free(session->app_name); + ast_free(session->remote_addr); + ast_free(session->channel_id); + ast_free(session->channel_name); + ao2_cleanup(session->owc); + session->owc = NULL; + if (!session->ast_ws_session) { return; } - ast_websocket_unref(ari_ws_session->ast_ws_session); - ari_ws_session->ast_ws_session = NULL; + ast_websocket_unref(session->ast_ws_session); + session->ast_ws_session = NULL; } -static int ari_ws_session_create( - int (*validator)(struct ast_json *), +#define handle_create_error(ser, code, msg, reason) \ +({ \ + if (ser) { \ + ast_http_error(ser, code, msg, reason); \ + } \ + ast_log(LOG_WARNING, "Failed to create ARI websocket session: %d %s %s\n", \ + code, msg, reason); \ +}) + +static struct ari_ws_session *session_create( struct ast_tcptls_session_instance *ser, - struct ast_ari_events_event_websocket_args *args, - const char *session_id) + const char *apps, + int subscribe_all, + const char *session_id, + struct ari_conf_outbound_websocket *ows, + enum ari_websocket_type ws_type) { - RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, ao2_cleanup); - int (* register_handler)(const char *, stasis_app_cb handler, void *data); - size_t size, i; + RAII_VAR(struct ari_ws_session *, session, NULL, ao2_cleanup); + size_t size; - if (validator == NULL) { - validator = null_validator; - } + ast_debug(3, "%s: Creating ARI websocket session for apps '%s'\n", + session_id, apps); - size = sizeof(*ari_ws_session) + strlen(session_id) + 1; + size = sizeof(*session) + strlen(session_id) + 1; - ari_ws_session = ao2_alloc(size, ari_ws_session_dtor); - if (!ari_ws_session) { - return -1; + session = ao2_alloc(size, session_dtor); + if (!session) { + return NULL; } - ari_ws_session->app_name = ast_strdup(args->app_parse); - if (!ari_ws_session->app_name) { - ast_http_error(ser, 500, "Internal Server Error", - "Allocation failed"); - return -1; - } + session->type = ws_type; + session->subscribe_all = subscribe_all; - strcpy(ari_ws_session->session_id, session_id); /* Safe */ + strcpy(session->session_id, session_id); /* Safe */ /* Instantiate the hash table for Stasis apps */ - ari_ws_session->websocket_apps = - ast_str_container_alloc(APPS_NUM_BUCKETS); - if (!ari_ws_session->websocket_apps) { - ast_http_error(ser, 500, "Internal Server Error", + if (AST_VECTOR_INIT(&session->websocket_apps, APPS_INIT_SIZE)) { + handle_create_error(ser, 500, "Internal Server Error", "Allocation failed"); - return -1; + return NULL; } /* Instantiate the message queue */ - if (AST_VECTOR_INIT(&ari_ws_session->message_queue, MESSAGES_INIT_SIZE)) { - ast_http_error(ser, 500, "Internal Server Error", + if (AST_VECTOR_INIT(&session->message_queue, MESSAGES_INIT_SIZE)) { + handle_create_error(ser, 500, "Internal Server Error", "Allocation failed"); - ao2_cleanup(ari_ws_session->websocket_apps); - return -1; + AST_VECTOR_FREE(&session->websocket_apps); + return NULL; } - /* Register the apps with Stasis */ - if (args->subscribe_all) { - register_handler = &stasis_app_register_all; - } else { - register_handler = &stasis_app_register; + session->validator = ari_validate_message_fn; + + if (ows) { + session->owc = ao2_bump(ows); } - for (i = 0; i < args->app_count; ++i) { - const char *app = args->app[i]; + if (session_register_apps(session, apps, subscribe_all) < 0) { + handle_create_error(ser, 500, "Internal Server Error", + "Stasis app registration failed"); + session_reset(session); + return NULL; + } - if (ast_strlen_zero(app)) { - ast_http_error(ser, 400, "Bad Request", - "Invalid application provided in param [app]."); - ari_ws_session_reset(ari_ws_session); - return -1; - } + if (!ao2_link(session_registry, session)) { + handle_create_error(ser, 500, "Internal Server Error", + "Allocation failed"); + session_reset(session); + return NULL; + } - if (ast_str_container_add(ari_ws_session->websocket_apps, app)) { - ast_http_error(ser, 500, "Internal Server Error", - "Allocation failed"); - ari_ws_session_reset(ari_ws_session); - return -1; - } + return ao2_bump(session); +} - if (register_handler(app, stasis_app_message_handler, ari_ws_session)) { - ast_log(LOG_WARNING, "Stasis registration failed for application: '%s'\n", app); - ast_http_error(ser, 500, "Internal Server Error", - "Stasis registration failed"); - ari_ws_session_reset(ari_ws_session); - return -1; - } +/*! + * \internal + * \brief Updates the websocket session. + * + * \details If the value of the \c ws_session is not \c NULL and there are messages in the + * event session's \c message_queue, the messages are dispatched and removed from + * the queue. + * + * \param ari_ws_session The ARI websocket session + * \param ast_ws_session The Asterisk websocket session + */ +static int session_update(struct ari_ws_session *ari_ws_session, + struct ast_websocket *ast_ws_session, int send_registered_events) +{ + RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); + int i; + + if (ast_ws_session == NULL) { + return -1; } - ari_ws_session->validator = validator; + if (!general) { + return -1; + } - /* - * Add the event session to the session registry. - * When this functions returns, the registry will have - * the only reference to the session. - */ - if (!ao2_link(ari_ws_session_registry, ari_ws_session)) { - ast_http_error(ser, 500, "Internal Server Error", - "Allocation failed"); - ari_ws_session_reset(ari_ws_session); + ari_ws_session->remote_addr = ast_strdup(ast_sockaddr_stringify( + ast_websocket_remote_address(ast_ws_session))); + if (!ari_ws_session->remote_addr) { + ast_log(LOG_ERROR, "Failed to copy remote address\n"); return -1; } + if (ast_websocket_set_nonblock(ast_ws_session) != 0) { + ast_log(LOG_ERROR, + "ARI web socket failed to set nonblock; closing: %s\n", + strerror(errno)); + return -1; + } + + if (ast_websocket_set_timeout(ast_ws_session, general->write_timeout)) { + ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n", + general->write_timeout); + } + + ao2_ref(ast_ws_session, +1); + ari_ws_session->ast_ws_session = ast_ws_session; + ao2_lock(ari_ws_session); + for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->message_queue); i++) { + struct ast_json *msg = AST_VECTOR_GET(&ari_ws_session->message_queue, i); + session_write(ari_ws_session, msg); + ast_json_unref(msg); + } + + AST_VECTOR_RESET(&ari_ws_session->message_queue, AST_VECTOR_ELEM_CLEANUP_NOOP); + ao2_unlock(ari_ws_session); + + if (send_registered_events) { + int i; + char *app; + + for (i = 0; i < AST_VECTOR_SIZE(&ari_ws_session->websocket_apps); i++) { + app = AST_VECTOR_GET(&ari_ws_session->websocket_apps, i); + session_send_app_event(ari_ws_session, + "ApplicationRegistered", app); + } + } + return 0; } /*! * \internal - * \brief This function gets called before the upgrade process is completed. - * HTTP is still in effect. + * \brief This function gets called for incoming websocket connections + * before the upgrade process is completed. + * + * The point is to be able to report early errors via HTTP rather + * than letting res_http_websocket create an ast_websocket session + * then immediately close it if there's an error. */ static int websocket_attempted_cb(struct ast_tcptls_session_instance *ser, struct ast_variable *get_params, struct ast_variable *headers, const char *session_id) { - struct ast_ari_events_event_websocket_args args = {}; - int res = 0; - RAII_VAR(struct ast_ari_response *, response, NULL, ast_free); - char *remote_addr = ast_sockaddr_stringify(&ser->remote_address); + const char *subscribe_all = NULL; + const char *apps = NULL; + struct ari_ws_session *session = NULL; - response = ast_calloc(1, sizeof(*response)); - if (!response) { - ast_log(LOG_ERROR, "Failed to create response.\n"); - ast_http_error(ser, 500, "Server Error", "Memory allocation error"); + apps = ast_variable_find_in_list(get_params, "app"); + if (ast_strlen_zero(apps)) { + handle_create_error(ser, 400, "Bad Request", + "HTTP request is missing param: [app]"); return -1; } - res = parse_app_args(get_params, response, &args); - if (res != 0) { - /* Param parsing failure */ - RAII_VAR(char *, msg, NULL, ast_json_free); - if (response->message) { - msg = ast_json_dump_string(response->message); - } else { - ast_log(LOG_ERROR, "Missing response message\n"); - } - - if (msg) { - ast_http_error(ser, response->response_code, response->response_text, msg); - return -1; - } - } + subscribe_all = ast_variable_find_in_list(get_params, "subscribeAll"); - if (args.app_count == 0) { - ast_http_error(ser, 400, "Bad Request", - "HTTP request is missing param: [app]"); + session = session_create(ser, apps, ast_true(subscribe_all), + session_id, NULL, ARI_WS_TYPE_INBOUND); + if (!session) { + handle_create_error(ser, 500, "Server Error", + "Failed to create ARI websocket session"); return -1; } + /* It's in the session registry now so we can release our reference */ + session_unref(session); -#if defined(AST_DEVMODE) - res = ari_ws_session_create(ast_ari_validate_message_fn(), - ser, &args, session_id); -#else - res = ari_ws_session_create(NULL, ser, &args, session_id); -#endif - if (res != 0) { - ast_log(LOG_ERROR, - "%s: Failed to create ARI ari_session\n", remote_addr); - } - - ast_free(args.app_parse); - ast_free(args.app); - return res; + return 0; } /*! * \internal - * \brief This function gets called after the upgrade process is completed. - * The websocket is now in effect. + * \brief This function gets called for incoming websocket connections + * after the upgrade process is completed. */ static void websocket_established_cb(struct ast_websocket *ast_ws_session, struct ast_variable *get_params, struct ast_variable *upgrade_headers) { - RAII_VAR(struct ast_ari_response *, response, NULL, ast_free); /* * ast_ws_session is passed in with it's refcount bumped so * we need to unref it when we're done. The refcount will * be bumped again when we add it to the ari_ws_session. */ RAII_VAR(struct ast_websocket *, s, ast_ws_session, ast_websocket_unref); - RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, ari_ws_session_cleanup); + RAII_VAR(struct ari_ws_session *, ari_ws_session, NULL, session_cleanup); struct ast_json *msg; struct ast_variable *v; char *remote_addr = ast_sockaddr_stringify( @@ -661,81 +816,819 @@ static void websocket_established_cb(struct ast_websocket *ast_ws_session, } } - response = ast_calloc(1, sizeof(*response)); - if (!response) { - SCOPE_EXIT_LOG_RTN(LOG_ERROR, - "%s: Failed to create response\n", remote_addr); - } - - /* Find the event_session and update its websocket */ - ari_ws_session = ao2_find(ari_ws_session_registry, session_id, OBJ_SEARCH_KEY); - if (ari_ws_session) { - ao2_unlink(ari_ws_session_registry, ari_ws_session); - ari_ws_session_update(ari_ws_session, ast_ws_session); - } else { + /* + * Find the ari_ws_session that was created by websocket_attempted_cb + * and update its ast_websocket. + */ + ari_ws_session = ao2_find(session_registry, session_id, OBJ_SEARCH_KEY); + if (!ari_ws_session) { SCOPE_EXIT_LOG_RTN(LOG_ERROR, - "%s: Failed to locate an event session for the websocket session\n", - remote_addr); + "%s: Failed to locate an event session for the websocket session %s\n", + remote_addr, session_id); } + /* + * Since this is a new inbound websocket session, + * session_register_apps() will have already sent "ApplicationRegistered" + * events for the apps. We don't want to do it again. + */ + session_update(ari_ws_session, ast_ws_session, 0); + + ari_ws_session->connected = 1; ast_trace(-1, "%s: Waiting for messages\n", remote_addr); - while ((msg = ari_ws_session_read(ari_ws_session))) { + while ((msg = session_read(ari_ws_session))) { ari_websocket_process_request(ari_ws_session, remote_addr, upgrade_headers, ari_ws_session->app_name, msg); ast_json_unref(msg); } + ari_ws_session->connected = 0; SCOPE_EXIT("%s: Websocket closed\n", remote_addr); } -static int ari_ws_session_shutdown_cb(void *ari_ws_session, void *arg, int flags) +static int session_shutdown_cb(void *obj, void *arg, int flags) { - ari_ws_session_cleanup(ari_ws_session); + struct ari_ws_session *session = obj; + + /* Per-call configs have no actual websocket */ + if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + ast_log(LOG_NOTICE, "%s: Shutting down %s ARI websocket session\n", + session->session_id, + ari_websocket_type_to_str(session->type)); + session_cleanup(session); + return 0; + } + if (session->type == ARI_WS_TYPE_INBOUND) { + ast_log(LOG_NOTICE, "%s: Shutting down inbound ARI websocket session from %s\n", + session->session_id, session->remote_addr); + } else { + ast_log(LOG_NOTICE, "%s: Shutting down %s ARI websocket session to %s\n", + session->session_id, + ari_websocket_type_to_str(session->type), + session->remote_addr); + } + + /* + * We need to ensure the session is kept around after the cleanup + * so we can close the websocket. + */ + ao2_bump(session); + session->closing = 1; + session_cleanup(session); + if (session->ast_ws_session) { + ast_websocket_close(session->ast_ws_session, 1000); + } return 0; } -static void ari_ws_session_registry_dtor(void) + +struct ari_ws_session * ari_websocket_get_session(const char *session_id) +{ + return ao2_find(session_registry, session_id, OBJ_SEARCH_KEY); +} + +static struct ari_ws_session *session_find_by_app(const char *app_name, + unsigned int ws_type) +{ + struct ari_ws_session *session = NULL; + struct ao2_iterator i; + + if (ast_strlen_zero(app_name)) { + return NULL; + } + + i = ao2_iterator_init(session_registry, 0); + while ((session = ao2_iterator_next(&i))) { + char *app = NULL; + if (!(session->type & ws_type)) { + session_unref(session); + continue; + } + + app = AST_VECTOR_GET_CMP(&session->websocket_apps, + app_name, ast_strings_equal); + if (app) { + break; + } + session_unref(session); + } + ao2_iterator_destroy(&i); + return session; +} + +/*! + * \internal + * \brief Connect to an external ARI application server with retries. + * + * Sets up the ast_websocket and tls config (if needed) and + * tries to open a connection to the remote server. + * + */ +static struct ast_websocket *outbound_session_connect( + struct ari_ws_session *session, + enum ast_websocket_result *result) +{ + struct ari_conf_outbound_websocket *owc = session->owc; + int reconnect_counter = owc->reconnect_attempts; + + while (1) { + struct ast_websocket *astws = NULL; + struct ast_websocket_client_options options = { + .uri = owc->uri, + .protocols = owc->protocols, + .username = owc->username, + .password = owc->password, + .timeout = owc->connect_timeout, + .suppress_connection_msgs = 1, + .tls_cfg = NULL, + }; + SCOPED_AO2LOCK(lock, session); + + if (owc->tls_enabled) { + /* + * tls_cfg and its contents are freed automatically + * by res_http_websocket when the connection ends. + * We create it even if tls is not enabled to we can + * suppress connection error messages and print our own. + */ + options.tls_cfg = ast_calloc(1, sizeof(*options.tls_cfg)); + if (!options.tls_cfg) { + return NULL; + } + /* TLS options */ + options.tls_cfg->enabled = owc->tls_enabled; + options.tls_cfg->cafile = ast_strdup(owc->ca_list_file); + options.tls_cfg->capath = ast_strdup(owc->ca_list_path); + options.tls_cfg->certfile = ast_strdup(owc->cert_file); + options.tls_cfg->pvtfile = ast_strdup(owc->priv_key_file); + ast_set2_flag(&options.tls_cfg->flags, !owc->verify_server_cert, AST_SSL_DONT_VERIFY_SERVER); + ast_set2_flag(&options.tls_cfg->flags, !owc->verify_server_hostname, AST_SSL_IGNORE_COMMON_NAME); + } + + astws = ast_websocket_client_create_with_options(&options, result); + if (astws && *result == WS_OK) { + return astws; + } + + reconnect_counter--; + if (reconnect_counter <= 0) { + ast_log(LOG_WARNING, + "%s: Failed after %d tries: %s%s%s%s\n", + session->session_id, + owc->reconnect_attempts, + ast_websocket_result_to_str(*result), + errno ? " (" : "", + errno ? strerror(errno) : "", + errno ? ")" : "" + ); + break; + } + usleep(owc->reconnect_interval * 1000); + } + + return NULL; +} + +/*! + * \internal + * \brief Connection and request handler thread for outbound websockets. + * + * This thread handles the connection and reconnection logic for outbound + * websockets. Once connected, it waits for incoming REST over Websocket + * requests and dispatches them to ari_websocket_process_request()). + */ +static void *outbound_session_handler_thread(void *obj) +{ + RAII_VAR(struct ari_ws_session *, session, obj, session_cleanup); + int already_sent_registers = 1; + + ast_debug(3, "%s: Starting outbound websocket thread RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + session->thread = pthread_self(); + session->connected = 0; + + while(1) { + RAII_VAR(struct ast_websocket *, astws, NULL, ast_websocket_unref); + RAII_VAR(struct ast_variable *, upgrade_headers, NULL, ast_variables_destroy); + enum ast_websocket_result result; + struct ast_json *msg; + + ast_debug(3, "%s: Attempting to connect to %s\n", session->session_id, + session->owc->uri); + + astws = outbound_session_connect(session, &result); + if (!astws || result != WS_OK) { + if (session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL) { + struct stasis_app_control *control = + stasis_app_control_find_by_channel_id(session->channel_id); + if (control) { + ast_debug(3, "%s: Connection failed. Returning to dialplan.\n", + session->session_id); + stasis_app_control_mark_failed(control); + stasis_app_control_continue(control, NULL, NULL, -1); + ao2_cleanup(control); + } else { + ast_debug(3, "%s: Connection failed. No control object found.\n", + session->session_id); + } + + break; + } + usleep(session->owc->reconnect_interval * 1000); + continue; + } + + /* + * We only want to send "ApplicationRegistered" events in the + * case of a reconnect. The initial connection will have already sent + * the events when outbound_register_apps() was called. + */ + session_update(session, astws, !already_sent_registers); + already_sent_registers = 0; + + /* + * This is the Authorization header that would normally be taken + * from the incoming HTTP request that is being upgraded to a websocket. + * Since this is an outbound websocket, we have to create it ourselves. + * + * This is NOT the same as the Authorization header that is used for + * authentication with the remote websocket server. + */ + upgrade_headers = ast_http_create_basic_auth_header( + session->owc->local_ari_user, session->owc->local_ari_password); + if (!upgrade_headers) { + ast_log(LOG_WARNING, "%s: Failed to create upgrade header\n", session->session_id); + session->thread = 0; + ast_websocket_close(astws, 1000); + return NULL; + } + + session->connected = 1; + ast_debug(3, "%s: Websocket connected\n", session->session_id); + ast_debug(3, "%s: Waiting for messages RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + + /* + * The websocket is connected. Now we need to wait for messages + * from the server. + */ + while ((msg = session_read(session))) { + ari_websocket_process_request(session, session->remote_addr, + upgrade_headers, session->app_name, msg); + ast_json_unref(msg); + } + + session->connected = 0; + ast_websocket_unref(session->ast_ws_session); + session->ast_ws_session = NULL; + if (session->closing) { + ast_debug(3, "%s: Websocket closing RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + break; + } + + ast_log(LOG_WARNING, "%s: Websocket disconnected. Reconnecting\n", + session->session_id); + } + + ast_debug(3, "%s: Stopping outbound websocket thread RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + session->thread = 0; + + return NULL; +} + +enum session_apply_result { + SESSION_APPLY_NO_CHANGE, + SESSION_APPLY_OK, + SESSION_APPLY_RECONNECT_REQUIRED, + SESSION_APPLY_FAILED, +}; + +static enum session_apply_result outbound_session_apply_config( + struct ari_ws_session *session, + struct ari_conf_outbound_websocket *new_owc) +{ + enum session_apply_result apply_result; + enum ari_conf_owc_fields what_changed; + const char *new_owc_id = ast_sorcery_object_get_id(new_owc); + + what_changed = ari_conf_owc_detect_changes(session->owc, new_owc); + + if (what_changed == ARI_OWC_FIELD_NONE) { + ast_debug(2, "%s: No changes detected\n", new_owc_id); + return SESSION_APPLY_NO_CHANGE; + } + ast_debug(2, "%s: Config change detected. Checking details\n", new_owc_id); + + if (what_changed & ARI_OWC_NEEDS_REREGISTER) { + ast_debug(2, "%s: Re-registering apps\n", new_owc_id); + + if (!(what_changed & ARI_OWC_FIELD_SUBSCRIBE_ALL)) { + /* + * If subscribe_all didn't change, we don't have to + * unregister apps that are already registered and + * also in the new config. We'll remove them from + * the session->websocket_apps container so that + * session_unregister_apps will only clean up + * the ones that are going away. session_register_apps + * will add them back in again and cause ApplicationReplaced + * messages to be sent. + * + * If subscribe_all did change, we have no choice but to + * unregister all apps and register all the ones in + * the new config even if they already existed. + */ + int i = 0; + char *app; + + while(i < (int) AST_VECTOR_SIZE(&session->websocket_apps)) { + app = AST_VECTOR_GET(&session->websocket_apps, i); + if (ast_in_delimited_string(app, new_owc->apps, ',')) { + AST_VECTOR_REMOVE_ORDERED(&session->websocket_apps, i); + ast_debug(3, "%s: Unlinked app '%s' to keep it from being unregistered\n", + new_owc_id, app); + ast_free(app); + } else { + i++; + } + } + } + + session_unregister_apps(session); + + /* + * Register the new apps. This will also replace any + * existing apps that are in the new config sending + * ApplicationRegistered or ApplicationReplaced events + * as necessary. + */ + if (session_register_apps(session, new_owc->apps, + new_owc->subscribe_all) < 0) { + ast_log(LOG_WARNING, "%s: Failed to register apps '%s'\n", + new_owc_id, new_owc->apps); + /* Roll back. */ + session_unregister_apps(session); + /* Re-register the original apps. */ + if (session_register_apps(session, session->owc->apps, + session->owc->subscribe_all) < 0) { + ast_log(LOG_WARNING, "%s: Failed to re-register apps '%s'\n", + new_owc_id, session->owc->apps); + } + return SESSION_APPLY_FAILED; + } + } + /* + * We need to update the session with the new config + * but it has to be done after re-registering apps and + * before we reconnect. + */ + ao2_replace(session->owc, new_owc); + session->type = new_owc->connection_type; + session->subscribe_all = new_owc->subscribe_all; + + apply_result = SESSION_APPLY_OK; + + if (what_changed & ARI_OWC_NEEDS_RECONNECT) { + ast_debug(2, "%s: Reconnect required\n", new_owc_id); + apply_result = SESSION_APPLY_RECONNECT_REQUIRED; + if (session->ast_ws_session) { + ast_debug(2, "%s: Closing websocket\n", new_owc_id); + ast_websocket_close(session->ast_ws_session, 1000); + } + } + + return apply_result; +} + +/* + * This is the fail-safe timeout for the per-call websocket + * connection. To prevent a cleanup race condition, we wait + * 3 times the timeout the thread will use to connect to the + * websocket server. This way we're sure the thread will be + * done before we do final cleanup. This timeout is only used + * if the thread is cancelled somehow and can't indicate + * whether it actually connected or not. + */ +#define PER_CALL_FAIL_SAFE_TIMEOUT(owc) \ + (int64_t)((owc->connect_timeout + owc->reconnect_interval) \ + * (owc->reconnect_attempts + 3)) + +/*! + * \brief This function gets called by app_stasis when a call arrives + * but a Stasis application isn't already registered. We check to see + * if a per-call config exists for the application and if so, we create a + * per-call websocket connection and return a unique app id which app_stasis + * can use to call stasis_app_exec() with. + */ +char *ast_ari_create_per_call_websocket(const char *app_name, + struct ast_channel *chan) +{ + RAII_VAR(struct ari_ws_session *, session, NULL, session_unref); + RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup); + RAII_VAR(char *, session_id, NULL, ast_free); + RAII_VAR(char *, app_id, NULL, ast_free); + enum ari_conf_owc_fields invalid_fields; + const char *owc_id = NULL; + char *app_id_rtn = NULL; + struct timeval tv_start; + int res = 0; + + owc = ari_conf_get_owc_for_app(app_name, ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG); + if (!owc) { + ast_log(LOG_WARNING, "%s: Failed to find outbound websocket per-call config for app '%s'\n", + ast_channel_name(chan), app_name); + return NULL; + } + owc_id = ast_sorcery_object_get_id(owc); + invalid_fields = ari_conf_owc_get_invalid_fields(owc_id); + + if (invalid_fields) { + ast_log(LOG_WARNING, "%s: Unable to create per-call websocket. Outbound websocket config is invalid\n", + owc_id); + return NULL; + } + + res = ast_asprintf(&session_id, "%s:%s", owc_id, ast_channel_name(chan)); + if (res < 0) { + return NULL; + } + res = ast_asprintf(&app_id, "%s:%s", app_name, ast_channel_name(chan)); + if (res < 0) { + ast_free(app_id); + return NULL; + } + + session = session_create(NULL, app_id, owc->subscribe_all, + session_id, owc, ARI_WS_TYPE_OUTBOUND_PER_CALL); + if (!session) { + ast_log(LOG_WARNING, "%s: Failed to create websocket session\n", session_id); + return NULL; + } + + session->channel_id = ast_strdup(ast_channel_uniqueid(chan)); + session->channel_name = ast_strdup(ast_channel_name(chan)); + + /* + * We have to bump the session reference count here because + * we need to check that the session is connected before we return. + * If it didn't connect, then the thread will have cleaned up the + * session while we're in the loop checking for the connection + * which will result in a SEGV or FRACK. + * RAII will clean up this bump. + */ + ao2_bump(session); + ast_debug(2, "%s: Starting thread RC: %d\n", session->session_id, + (int)ao2_ref(session, 0)); + + if (ast_pthread_create_detached_background(&session->thread, NULL, + outbound_session_handler_thread, session)) { + session_cleanup(session); + ast_log(LOG_WARNING, "%s: Failed to create thread.\n", session->session_id); + return NULL; + } + + /* + * We need to make sure the session connected and is processing + * requests before we return but we don't want to block forever + * in case the thread never starts or gets cancelled so we have + * a fail-safe timeout. + */ + tv_start = ast_tvnow(); + while (session->thread > 0 && !session->connected) { + struct timeval tv_now = ast_tvnow(); + if (ast_tvdiff_ms(tv_now, tv_start) > PER_CALL_FAIL_SAFE_TIMEOUT(owc)) { + break; + } + /* Sleep for 500ms before checking again. */ + usleep(500 * 1000); + } + + if (session->thread <= 0 || !session->connected) { + ast_log(LOG_WARNING, "%s: Failed to create per call websocket thread\n", + session_id); + return NULL; + } + + ast_debug(3, "%s: Created per call websocket for app '%s'\n", + session_id, app_id); + + /* + * We now need to prevent RAII from freeing the app_id. + */ + app_id_rtn = app_id; + app_id = NULL; + return app_id_rtn; +} + +#define STASIS_END_MAX_WAIT_MS 5000 +#define STASIS_END_POST_WAIT_US (3000 * 1000) + +/* + * This thread is used to close the websocket after the StasisEnd + * event has been sent and control has been returned to the dialplan. + * We wait a few seconds to allow additional events to be sent + * like ChannelVarset and ChannelDestroyed. + */ +static void *outbound_session_pc_close_thread(void *data) { - if (!ari_ws_session_registry) { + /* + * We're using RAII because we want to show a debug message + * after we run ast_websocket_close(). + */ + RAII_VAR(struct ari_ws_session *, session, data, session_unref); + + /* + * We're going to wait 3 seconds to allow stasis to send additional + * events like ChannelVarset and ChannelDestroyed after the StasisEnd. + */ + ast_debug(3, "%s: Waiting for %dms before closing websocket RC: %d\n", + session->session_id, (int)(STASIS_END_POST_WAIT_US / 1000), + (int)ao2_ref(session, 0)); + usleep(STASIS_END_POST_WAIT_US); + session->closing = 1; + if (session->ast_ws_session) { + ast_websocket_close(session->ast_ws_session, 1000); + } + ast_debug(3, "%s: Websocket closed RC: %d\n", session->session_id, + (int)ao2_ref(session, 0)); + return NULL; +} + +/*! + * \brief This function is called by the app_stasis dialplan app + * to close a per-call websocket after stasis_app_exec() returns. + */ +void ast_ari_close_per_call_websocket(char *app_name) +{ + struct ari_ws_session *session = NULL; + pthread_t thread; + struct timeval tv_start; + + session = session_find_by_app(app_name, ARI_WS_TYPE_OUTBOUND_PER_CALL); + if (!session) { + ast_debug(3, "%s: Per call websocket not found\n", app_name); + ast_free(app_name); return; } + ast_free(app_name); + + /* + * When stasis_app_exec() returns, the StasisEnd event for the + * channel has been queued but since actually sending it is done + * in a separate thread, it probably won't have been sent yet. + * We need to wait for it to go out on the wire before we close the + * websocket. ari_websocket_send_event will set a flag on the session + * when a StasisEnd event is sent for the channel that originally + * triggered the connection. We'll wait for that but we don't want + * to wait forever so there's a fail-safe timeout in case a thread + * got cancelled or we missed the StasisEnd event somehow. + */ + ast_debug(3, "%s: Waiting for StasisEnd event to be sent RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + + tv_start = ast_tvnow(); + while (session->thread > 0 && !session->stasis_end_sent) { + struct timeval tv_now = ast_tvnow(); + int64_t diff = ast_tvdiff_ms(tv_now, tv_start); + ast_debug(3, "%s: Waiting for StasisEnd event %lu %d %ld\n", + session->session_id, (unsigned long)session->thread, + session->stasis_end_sent, diff); + if (diff > STASIS_END_MAX_WAIT_MS) { + break; + } + /* Sleep for 500ms before checking again. */ + usleep(500 * 1000); + } + ast_debug(3, "%s: StasisEnd event sent. Scheduling websocket close. RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + + /* + * We can continue to send events like ChannelVarset and ChannelDestroyed + * to the websocket after the StasisEnd event but those events won't be + * generated until after the Stasis() dialplan app returns. We don't want + * to hold up the dialplan while we wait so we'll create a thread that waits + * a few seconds more before closing the websocket. + * + * We transferring ownership of the session to the thread. + */ + if (ast_pthread_create_detached_background(&thread, NULL, + outbound_session_pc_close_thread, session)) { + ast_log(LOG_WARNING, "%s: Failed to create websocket close thread\n", + session->session_id); + session_unref(session); + } + ast_debug(3, "%s: Scheduled websocket close RC: %d\n", + session->session_id, (int)ao2_ref(session, 0)); + + return; +} + +struct ao2_container* ari_websocket_get_sessions(void) +{ + return ao2_bump(session_registry); +} + +static int outbound_session_create(void *obj, void *args, int flags) +{ + struct ari_conf_outbound_websocket *owc = obj; + const char *owc_id = ast_sorcery_object_get_id(owc); + struct ari_ws_session *session = NULL; + enum session_apply_result apply_result; + enum ari_conf_owc_fields invalid_fields = ari_conf_owc_get_invalid_fields(owc_id); + + ast_log(LOG_NOTICE, "%s: Creating outbound websocket session\n", owc_id); + + session = ari_websocket_get_session(owc_id); + if (session) { + ast_debug(2, "%s: Found existing connection\n", owc_id); + if (invalid_fields) { + session_unref(session); + ast_log(LOG_WARNING, + "%s: Unable to update websocket session. Outbound websocket config is invalid\n", + owc_id); + return 0; + } + + ao2_lock(session); + apply_result = outbound_session_apply_config(session, owc); + ao2_unlock(session); + session_unref(session); + if (apply_result == SESSION_APPLY_FAILED) { + ast_log(LOG_WARNING, + "%s: Failed to apply new configuration. Existing connection preserved.\n", + owc_id); + } + return 0; + } + + if (invalid_fields) { + ast_log(LOG_WARNING, + "%s: Unable to create websocket session. Outbound websocket config is invalid\n", + owc_id); + return 0; + } - ao2_callback(ari_ws_session_registry, OBJ_MULTIPLE | OBJ_NODATA, - ari_ws_session_shutdown_cb, NULL); + session = session_create(NULL, owc->apps, owc->subscribe_all, owc_id, + owc, owc->connection_type); + if (!session) { + ast_log(LOG_WARNING, "%s: Failed to create websocket session\n", owc_id); + return 0; + } + + if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + /* There's no thread to transfer the reference to */ + session_unref(session); + return 0; + } - ao2_cleanup(ari_ws_session_registry); - ari_ws_session_registry = NULL; + ast_debug(2, "%s: Starting thread RC: %d\n", session->session_id, + (int)ao2_ref(session, 0)); + /* We're transferring the session reference to the thread. */ + if (ast_pthread_create_detached_background(&session->thread, NULL, + outbound_session_handler_thread, session)) { + session_cleanup(session); + ast_log(LOG_WARNING, "%s: Failed to create thread.\n", session->session_id); + return 0; + } + ast_debug(2, "%s: launched thread\n", session->session_id); + + return 0; +} + +static void outbound_sessions_load(const char *name) +{ + RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup); + struct ao2_iterator i; + struct ari_ws_session *session; + + ast_debug(2, "Reloading ARI websockets\n"); + + ao2_callback(owcs, OBJ_NODATA, outbound_session_create, NULL); + + i = ao2_iterator_init(session_registry, 0); + while ((session = ao2_iterator_next(&i))) { + int cleanup = 1; + if (session->owc + && (session->type & + (ARI_WS_TYPE_OUTBOUND_PERSISTENT | ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG))) { + struct ari_conf_outbound_websocket *ows = + ari_conf_get_owc(session->session_id); + if (!ows) { + ast_debug(3, "Cleaning up outbound websocket %s\n", + session->session_id); + session->closing = 1; + session_cleanup(session); + if (session->ast_ws_session) { + ast_websocket_close(session->ast_ws_session, 1000); + } + + if (session->type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) { + /* + * If persistent, session_cleanup will cleanup + * this reference so we don't want to double clean it up. + * session_cleanup doesn't cleanup the reference + * for per-call configs so we need to do that ourselves. + */ + cleanup = 0; + } + } + ao2_cleanup(ows); + } + /* We don't want to double cleanup if its been closed. */ + if (cleanup) { + ao2_cleanup(session); + } + } + ao2_iterator_destroy(&i); + + return; +} + +int ari_outbound_websocket_start(struct ari_conf_outbound_websocket *owc) +{ + if (owc) { + return outbound_session_create(owc, NULL, 0); + } + return -1; +} + +void ari_websocket_shutdown(struct ari_ws_session *session) +{ + if (session) { + session_shutdown_cb(session, NULL, 0); + } +} + +void ari_websocket_shutdown_all(void) +{ + if (session_registry) { + ao2_callback(session_registry, OBJ_MULTIPLE | OBJ_NODATA, + session_shutdown_cb, NULL); + } +} + +static void session_registry_dtor(void) +{ + if (session_registry) { + ao2_callback(session_registry, OBJ_MULTIPLE | OBJ_NODATA, + session_shutdown_cb, NULL); + ao2_cleanup(session_registry); + session_registry = NULL; + } } int ari_websocket_unload_module(void) { - ari_ws_session_registry_dtor(); + session_registry_dtor(); ao2_cleanup(ast_ws_server); ast_ws_server = NULL; return 0; } -AO2_STRING_FIELD_CMP_FN(ari_ws_session, session_id); -AO2_STRING_FIELD_HASH_FN(ari_ws_session, session_id); +static struct ast_sorcery_observer observer_callbacks = { + .loaded = outbound_sessions_load, +}; + +AO2_STRING_FIELD_CMP_FN(ari_ws_session, session_id) +AO2_STRING_FIELD_SORT_FN(ari_ws_session, session_id) -int ari_websocket_load_module(void) +int ari_websocket_load_module(int is_enabled) { int res = 0; struct ast_websocket_protocol *protocol; - ari_ws_session_registry = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, - ARI_WS_SESSION_NUM_BUCKETS, ari_ws_session_hash_fn, - NULL, ari_ws_session_cmp_fn); - if (!ari_ws_session_registry) { + ast_debug(2, "Initializing ARI websockets. Enabled: %s\n", is_enabled ? "yes" : "no"); + + session_registry = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_MUTEX, + AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, + ari_ws_session_sort_fn, ari_ws_session_cmp_fn); + if (!session_registry) { ast_log(LOG_WARNING, "Failed to allocate the local registry for websocket applications\n"); return AST_MODULE_LOAD_DECLINE; } + res = ari_sorcery_observer_add("outbound_websocket", &observer_callbacks); + if (res < 0) { + ast_log(LOG_WARNING, "Failed to register ARI websocket observer\n"); + session_registry_dtor(); + return AST_MODULE_LOAD_DECLINE; + } + + /* + * The global "enabled" flag only controls whether the REST and + * inbound websockets are enabled. The outbound websocket + * configs are always enabled. + if (!is_enabled) { + return AST_MODULE_LOAD_SUCCESS; + } + */ + ast_ws_server = ast_websocket_server_create(); if (!ast_ws_server) { - ari_ws_session_registry_dtor(); + session_registry_dtor(); return AST_MODULE_LOAD_DECLINE; } @@ -743,7 +1636,7 @@ int ari_websocket_load_module(void) if (!protocol) { ao2_ref(ast_ws_server, -1); ast_ws_server = NULL; - ari_ws_session_registry_dtor(); + session_registry_dtor(); return AST_MODULE_LOAD_DECLINE; } protocol->session_attempted = websocket_attempted_cb; diff --git a/res/ari/ari_websockets.h b/res/ari/ari_websockets.h index 4ad180acfd..24b4ba3f1f 100644 --- a/res/ari/ari_websockets.h +++ b/res/ari/ari_websockets.h @@ -35,19 +35,54 @@ struct ast_ari_events_event_websocket_args; * which causes optional_api stuff to happen, which makes optional_api more * difficult to debug. */ -//struct ast_websocket_server; struct ast_websocket; +/* + * Since we create a "stasis-" dialplan context for each + * stasis app, we need to make sure that the total length will be + * <= AST_MAX_CONTEXT + */ +#define STASIS_CONTEXT_PREFIX "stasis-" +#define STASIS_CONTEXT_PREFIX_LEN (sizeof(STASIS_CONTEXT_PREFIX) - 1) +#define ARI_MAX_APP_NAME_LEN (AST_MAX_CONTEXT - STASIS_CONTEXT_PREFIX_LEN) + + +enum ari_websocket_type { + ARI_WS_TYPE_OUTBOUND_PERSISTENT = (1 << 0), + ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG = (1 << 1), + ARI_WS_TYPE_OUTBOUND_PER_CALL = (1 << 2), + ARI_WS_TYPE_INBOUND = (1 << 3), + ARI_WS_TYPE_ANY = (0xFFFFFFFF), +}; + struct ari_ws_session { + enum ari_websocket_type type; /*!< The type of websocket session. */ struct ast_websocket *ast_ws_session; /*!< The parent websocket session. */ int (*validator)(struct ast_json *); /*!< The message validator. */ - struct ao2_container *websocket_apps; /*!< List of Stasis apps registered to + struct ast_vector_string websocket_apps; /*!< List of Stasis apps registered to the websocket session. */ + int subscribe_all; /*!< Flag indicating if all events are subscribed to. */ AST_VECTOR(, struct ast_json *) message_queue; /*!< Container for holding delayed messages. */ char *app_name; /*!< The name of the Stasis application. */ + char *remote_addr; /*!< The remote address. */ + struct ari_conf_outbound_websocket *owc; /*!< The outbound websocket configuration. */ + pthread_t thread; /*!< The thread that handles the websocket. */ + char *channel_id; /*!< The channel id for per-call websocket. */ + char *channel_name; /*!< The channel name for per-call websocket. */ + int stasis_end_sent; /*!< Flag indicating if the StasisEnd message was sent. */ + int connected; /*!< Flag indicating if the websocket is connected. */ + int closing; /*!< Flag indicating if the session is closing. */ char session_id[]; /*!< The id for the websocket session. */ }; +struct ao2_container* ari_websocket_get_sessions(void); +struct ari_ws_session *ari_websocket_get_session(const char *session_id); +struct ari_ws_session *ari_websocket_get_session_by_app(const char *app_name); +const char *ari_websocket_type_to_str(enum ari_websocket_type type); +void ari_websocket_shutdown(struct ari_ws_session *session); +void ari_websocket_shutdown_all(void); +int ari_outbound_websocket_start(struct ari_conf_outbound_websocket *owc); + /*! * \internal * \brief Send a JSON event to a websocket. @@ -91,6 +126,6 @@ void ari_handle_websocket(struct ast_tcptls_session_instance *ser, struct ast_variable *headers); int ari_websocket_unload_module(void); -int ari_websocket_load_module(void); +int ari_websocket_load_module(int is_enabled); #endif /* ARI_WEBSOCKETS_H_ */ diff --git a/res/ari/cli.c b/res/ari/cli.c index f9d9cecfb7..306a597b32 100644 --- a/res/ari/cli.c +++ b/res/ari/cli.c @@ -27,11 +27,13 @@ #include "asterisk/astobj2.h" #include "asterisk/cli.h" #include "asterisk/stasis_app.h" +#include "asterisk/uuid.h" #include "internal.h" +#include "ari_websockets.h" static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); + RAII_VAR(struct ari_conf_general *, general, NULL, ao2_cleanup); switch (cmd) { case CLI_INIT: @@ -50,43 +52,42 @@ static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) return CLI_SHOWUSAGE; } - conf = ast_ari_config_get(); + general = ari_conf_get_general(); - if (!conf) { + if (!general) { ast_cli(a->fd, "Error getting ARI configuration\n"); return CLI_FAILURE; } ast_cli(a->fd, "ARI Status:\n"); - ast_cli(a->fd, "Enabled: %s\n", AST_CLI_YESNO(conf->general->enabled)); + ast_cli(a->fd, "Enabled: %s\n", AST_CLI_YESNO(general->enabled)); ast_cli(a->fd, "Output format: "); - if (conf->general->format & AST_JSON_PRETTY) { + if (general->format & AST_JSON_PRETTY) { ast_cli(a->fd, "pretty"); } else { ast_cli(a->fd, "compact"); } ast_cli(a->fd, "\n"); - ast_cli(a->fd, "Auth realm: %s\n", conf->general->auth_realm); - ast_cli(a->fd, "Allowed Origins: %s\n", conf->general->allowed_origins); - ast_cli(a->fd, "User count: %d\n", ao2_container_count(conf->users)); + ast_cli(a->fd, "Auth realm: %s\n", general->auth_realm); + ast_cli(a->fd, "Allowed Origins: %s\n", general->allowed_origins); return CLI_SUCCESS; } static int show_users_cb(void *obj, void *arg, int flags) { - struct ast_ari_conf_user *user = obj; + struct ari_conf_user *user = obj; struct ast_cli_args *a = arg; ast_cli(a->fd, "%-4s %s\n", AST_CLI_YESNO(user->read_only), - user->username); + ast_sorcery_object_get_id(user)); return 0; } static char *ari_show_users(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, users, NULL, ao2_cleanup); switch (cmd) { case CLI_INIT: @@ -105,8 +106,8 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd, return CLI_SHOWUSAGE; } - conf = ast_ari_config_get(); - if (!conf) { + users = ari_conf_get_users(); + if (!users) { ast_cli(a->fd, "Error getting ARI configuration\n"); return CLI_FAILURE; } @@ -114,64 +115,38 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd, ast_cli(a->fd, "r/o? Username\n"); ast_cli(a->fd, "---- --------\n"); - ao2_callback(conf->users, OBJ_NODATA, show_users_cb, a); + ao2_callback(users, OBJ_NODATA, show_users_cb, a); return CLI_SUCCESS; } -struct user_complete { - /*! Nth user to search for */ - int state; - /*! Which user currently on */ - int which; -}; - -static int complete_ari_user_search(void *obj, void *arg, void *data, int flags) +static void complete_sorcery_object(struct ao2_container *container, + const char *word) { - struct user_complete *search = data; - - if (++search->which > search->state) { - return CMP_MATCH; - } - return 0; + size_t wordlen = strlen(word); + void *object; + struct ao2_iterator i = ao2_iterator_init(container, 0); + + while ((object = ao2_iterator_next(&i))) { + const char *id = ast_sorcery_object_get_id(object); + if (!strncasecmp(word, id, wordlen)) { + ast_cli_completion_add(ast_strdup(id)); + } + ao2_ref(object, -1); + } + ao2_iterator_destroy(&i); } -static char *complete_ari_user(struct ast_cli_args *a) +static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); - RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); - - struct user_complete search = { - .state = a->n, - }; + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, users, ari_conf_get_users(), ao2_cleanup); - conf = ast_ari_config_get(); - if (!conf) { + if (!users) { ast_cli(a->fd, "Error getting ARI configuration\n"); return CLI_FAILURE; } - user = ao2_callback_data(conf->users, - ast_strlen_zero(a->word) ? 0 : OBJ_PARTIAL_KEY, - complete_ari_user_search, (char*)a->word, &search); - - return user ? ast_strdup(user->username) : NULL; -} - -static char *complete_ari_show_user(struct ast_cli_args *a) -{ - if (a->pos == 3) { - return complete_ari_user(a); - } - - return NULL; -} - -static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); - RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); - switch (cmd) { case CLI_INIT: e->command = "ari show user"; @@ -180,7 +155,8 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args " Shows a specific ARI user\n"; return NULL; case CLI_GENERATE: - return complete_ari_show_user(a); + complete_sorcery_object(users, a->word); + return NULL; default: break; } @@ -189,20 +165,13 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args return CLI_SHOWUSAGE; } - conf = ast_ari_config_get(); - - if (!conf) { - ast_cli(a->fd, "Error getting ARI configuration\n"); - return CLI_FAILURE; - } - - user = ao2_find(conf->users, a->argv[3], OBJ_KEY); + user = ari_conf_get_user(a->argv[3]); if (!user) { ast_cli(a->fd, "User '%s' not found\n", a->argv[3]); return CLI_SUCCESS; } - ast_cli(a->fd, "Username: %s\n", user->username); + ast_cli(a->fd, "Username: %s\n", ast_sorcery_object_get_id(user)); ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only)); return CLI_SUCCESS; @@ -281,7 +250,7 @@ static char *ari_show_apps(struct ast_cli_entry *e, int cmd, struct ast_cli_args ast_cli(a->fd, "=========================\n"); it_apps = ao2_iterator_init(apps, 0); while ((app = ao2_iterator_next(&it_apps))) { - ast_cli(a->fd, "%-25.25s\n", app); + ast_cli(a->fd, "%s\n", app); ao2_ref(app, -1); } @@ -291,56 +260,32 @@ static char *ari_show_apps(struct ast_cli_entry *e, int cmd, struct ast_cli_args return CLI_SUCCESS; } -struct app_complete { - /*! Nth app to search for */ - int state; - /*! Which app currently on */ - int which; -}; - -static int complete_ari_app_search(void *obj, void *arg, void *data, int flags) +static void complete_app(struct ao2_container *container, + const char *word) { - struct app_complete *search = data; + size_t wordlen = strlen(word); + void *object; + struct ao2_iterator i = ao2_iterator_init(container, 0); - if (++search->which > search->state) { - return CMP_MATCH; + while ((object = ao2_iterator_next(&i))) { + if (!strncasecmp(word, object, wordlen)) { + ast_cli_completion_add(ast_strdup(object)); + } + ao2_ref(object, -1); } - return 0; + ao2_iterator_destroy(&i); } -static char *complete_ari_app(struct ast_cli_args *a, int include_all) +static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { + void *app; RAII_VAR(struct ao2_container *, apps, stasis_app_get_all(), ao2_cleanup); - RAII_VAR(char *, app, NULL, ao2_cleanup); - - struct app_complete search = { - .state = a->n, - }; - - if (a->pos != 3) { - return NULL; - } if (!apps) { ast_cli(a->fd, "Error getting ARI applications\n"); return CLI_FAILURE; } - if (include_all && ast_strlen_zero(a->word)) { - ast_str_container_add(apps, " all"); - } - - app = ao2_callback_data(apps, - ast_strlen_zero(a->word) ? 0 : OBJ_SEARCH_PARTIAL_KEY, - complete_ari_app_search, (char*)a->word, &search); - - return app ? ast_strdup(app) : NULL; -} - -static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - void *app; - switch (cmd) { case CLI_INIT: e->command = "ari show app"; @@ -350,7 +295,8 @@ static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args ; return NULL; case CLI_GENERATE: - return complete_ari_app(a, 0); + complete_app(apps, a->word); + return NULL; default: break; } @@ -373,9 +319,15 @@ static char *ari_show_app(struct ast_cli_entry *e, int cmd, struct ast_cli_args static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { + RAII_VAR(struct ao2_container *, apps, stasis_app_get_all(), ao2_cleanup); void *app; int debug; + if (!apps) { + ast_cli(a->fd, "Error getting ARI applications\n"); + return CLI_FAILURE; + } + switch (cmd) { case CLI_INIT: e->command = "ari set debug"; @@ -385,7 +337,14 @@ static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args ; return NULL; case CLI_GENERATE: - return complete_ari_app(a, 1); + if (a->argc == 3) { + ast_cli_completion_add(ast_strdup("all")); + complete_app(apps, a->word); + } else if (a->argc == 4) { + ast_cli_completion_add(ast_strdup("on")); + ast_cli_completion_add(ast_strdup("off")); + } + return NULL; default: break; } @@ -418,6 +377,309 @@ static char *ari_set_debug(struct ast_cli_entry *e, int cmd, struct ast_cli_args return CLI_SUCCESS; } +static int show_owc_cb(void *obj, void *arg, int flags) +{ + struct ari_conf_outbound_websocket *owc = obj; + const char *id = ast_sorcery_object_get_id(owc); + enum ari_conf_owc_fields invalid_fields = ari_conf_owc_get_invalid_fields(id); + struct ast_cli_args *a = arg; + + ast_cli(a->fd, "%-32s %-15s %-32s %-7s %s\n", + id, + ari_websocket_type_to_str(owc->connection_type), + owc->apps, + invalid_fields == ARI_OWC_FIELD_NONE ? "valid" : "INVALID", + owc->uri); + return 0; +} + +#define DASHES "----------------------------------------------------------------------" +static char *ari_show_owcs(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup); + + switch (cmd) { + case CLI_INIT: + e->command = "ari show outbound-websockets"; + e->usage = + "Usage: ari show outbound-websockets\n" + " Shows all ARI outbound-websockets\n"; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + owcs = ari_conf_get_owcs(); + if (!owcs) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "%-32s %-15s %-32s %-7s %s\n", "Name", "Type", "Apps", "Status", "URI"); + ast_cli(a->fd, "%.*s %.*s %.*s %.*s %.*s\n", 32, DASHES, 15, DASHES, 32, DASHES, 7, DASHES, 64, DASHES); + + ao2_callback(owcs, OBJ_NODATA, show_owc_cb, a); + + return CLI_SUCCESS; +} + +static char *ari_show_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup); + const char *id = NULL; + enum ari_conf_owc_fields invalid_fields; + + switch (cmd) { + case CLI_INIT: + e->command = "ari show outbound-websocket"; + e->usage = + "Usage: ari show outbound-websocket \n" + " Shows a specific ARI outbound websocket\n"; + return NULL; + case CLI_GENERATE: + complete_sorcery_object(owcs, a->word); + return NULL; + default: + break; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + owc = ari_conf_get_owc(a->argv[3]); + if (!owc) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + id = ast_sorcery_object_get_id(owc); + invalid_fields = ari_conf_owc_get_invalid_fields(id); + + ast_cli(a->fd, "[%s] %s\n", id, + invalid_fields == ARI_OWC_FIELD_NONE ? "" : "**INVALID**"); + ast_cli(a->fd, "uri = %s\n", owc->uri); + ast_cli(a->fd, "protocols = %s\n", owc->protocols); + ast_cli(a->fd, "apps = %s%s\n", owc->apps, + invalid_fields & ARI_OWC_FIELD_APPS ? " (invalid)" : ""); + ast_cli(a->fd, "username = %s\n", owc->username); + ast_cli(a->fd, "password = %s\n", S_COR(owc->password, "********", "")); + ast_cli(a->fd, "local_ari_user = %s%s\n", owc->local_ari_user, + invalid_fields & ARI_OWC_FIELD_LOCAL_ARI_USER ? " (invalid)" : ""); + ast_cli(a->fd, "connection_type = %s\n", ari_websocket_type_to_str(owc->connection_type)); + ast_cli(a->fd, "subscribe_all = %s\n", AST_CLI_YESNO(owc->subscribe_all)); + ast_cli(a->fd, "connec_timeout = %d\n", owc->connect_timeout); + ast_cli(a->fd, "reconnect_attempts = %d\n", owc->reconnect_attempts); + ast_cli(a->fd, "reconnect_interval = %d\n", owc->reconnect_interval); + ast_cli(a->fd, "tls_enabled = %s\n", AST_CLI_YESNO(owc->tls_enabled)); + ast_cli(a->fd, "ca_list_file = %s\n", owc->ca_list_file); + ast_cli(a->fd, "ca_list_path = %s\n", owc->ca_list_path); + ast_cli(a->fd, "cert_file = %s\n", owc->cert_file); + ast_cli(a->fd, "priv_key_file = %s\n", owc->priv_key_file); + ast_cli(a->fd, "verify_server = %s\n", AST_CLI_YESNO(owc->verify_server_cert)); + ast_cli(a->fd, "verify_server = %s\n", AST_CLI_YESNO(owc->verify_server_cert)); + ast_cli(a->fd, "\n"); + + return CLI_SUCCESS; +} + +static char *ari_start_owc(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ari_conf_outbound_websocket *, owc, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup); + + if (!owcs) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE ; + } + + switch (cmd) { + case CLI_INIT: + e->command = "ari start outbound-websocket"; + e->usage = + "Usage: ari start outbound-websocket \n" + " Starts a specific ARI outbound websocket\n"; + return NULL; + case CLI_GENERATE: + complete_sorcery_object(owcs, a->word); + return NULL; + default: + break; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + owc = ari_conf_get_owc(a->argv[3]); + if (!owc) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE; + } + ast_cli(a->fd, "Starting websocket session for outbound-websocket '%s'\n", a->argv[3]); + + if (ari_outbound_websocket_start(owc) != 0) { + ast_cli(a->fd, "Error starting outbound websocket\n"); + return CLI_FAILURE ; + } + + return CLI_SUCCESS; +} + +static int show_sessions_cb(void *obj, void *arg, int flags) +{ + struct ari_ws_session *session = obj; + struct ast_cli_args *a = arg; + char *apps = ast_vector_string_join(&session->websocket_apps, ","); + + ast_cli(a->fd, "%-*s %-15s %-32s %-5s %s\n", + AST_UUID_STR_LEN, + session->session_id, + ari_websocket_type_to_str(session->type), + S_OR(session->remote_addr, "N/A"), + session->type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG + ? "N/A" : (session->connected ? "Up" : "Down"), + S_OR(apps, "")); + + ast_free(apps); + return 0; +} + +#define DASHES "----------------------------------------------------------------------" +static char *ari_show_sessions(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + RAII_VAR(struct ao2_container *, sessions, NULL, ao2_cleanup); + + switch (cmd) { + case CLI_INIT: + e->command = "ari show websocket sessions"; + e->usage = + "Usage: ari show websocket sessions\n" + " Shows all ARI websocket sessions\n"; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + sessions = ari_websocket_get_sessions(); + if (!sessions) { + ast_cli(a->fd, "Error getting websocket sessions\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n", + AST_UUID_STR_LEN, AST_UUID_STR_LEN, + "Connection ID", + "Type", + "RemoteAddr", + "State", + "Apps" + ); + ast_cli(a->fd, "%-*.*s %-15.15s %-32.32s %-5.5s %-16.16s\n", + AST_UUID_STR_LEN, AST_UUID_STR_LEN, DASHES, DASHES, DASHES, DASHES, DASHES); + + ao2_callback(sessions, OBJ_NODATA, show_sessions_cb, a); + + return CLI_SUCCESS; +} + +static char *ari_shut_sessions(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + + switch (cmd) { + case CLI_INIT: + e->command = "ari shutdown websocket sessions"; + e->usage = + "Usage: ari shutdown websocket sessions\n" + " Shuts down all ARI websocket sessions\n"; + return NULL; + case CLI_GENERATE: + return NULL; + default: + break; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + ast_cli(a->fd, "Shutting down all websocket sessions\n"); + ari_websocket_shutdown_all(); + + return CLI_SUCCESS; +} + +static void complete_session(struct ao2_container *container, + const char *word) +{ + size_t wordlen = strlen(word); + struct ari_ws_session *session; + struct ao2_iterator i = ao2_iterator_init(container, 0); + + while ((session = ao2_iterator_next(&i))) { + if (!strncasecmp(word, session->session_id, wordlen)) { + ast_cli_completion_add(ast_strdup(session->session_id)); + } + ao2_ref(session, -1); + } + ao2_iterator_destroy(&i); +} + +static char *ari_shut_session(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + RAII_VAR(struct ari_ws_session *, session, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, sessions, ari_websocket_get_sessions(), ao2_cleanup); + + if (!sessions) { + ast_cli(a->fd, "Error getting ARI configuration\n"); + return CLI_FAILURE ; + } + + switch (cmd) { + case CLI_INIT: + e->command = "ari shutdown websocket session"; + e->usage = + "Usage: ari shutdown websocket session \n" + " Shuts down ARI websocket session\n"; + return NULL; + case CLI_GENERATE: + complete_session(sessions, a->word); + return NULL; + default: + break; + } + + if (a->argc != 5) { + return CLI_SHOWUSAGE; + } + + session = ari_websocket_get_session(a->argv[4]); + if (!session) { + ast_cli(a->fd, "Websocket session '%s' not found\n", a->argv[4]); + return CLI_FAILURE ; + } + ast_cli(a->fd, "Shutting down websocket session '%s'\n", a->argv[4]); + ari_websocket_shutdown(session); + + return CLI_SUCCESS; +} + static struct ast_cli_entry cli_ari[] = { AST_CLI_DEFINE(ari_show, "Show ARI settings"), AST_CLI_DEFINE(ari_show_users, "List ARI users"), @@ -426,12 +688,18 @@ static struct ast_cli_entry cli_ari[] = { AST_CLI_DEFINE(ari_show_apps, "List registered ARI applications"), AST_CLI_DEFINE(ari_show_app, "Display details of a registered ARI application"), AST_CLI_DEFINE(ari_set_debug, "Enable/disable debugging of an ARI application"), + AST_CLI_DEFINE(ari_show_owcs, "List outbound websocket connections"), + AST_CLI_DEFINE(ari_show_owc, "Show outbound websocket connection"), + AST_CLI_DEFINE(ari_start_owc, "Start outbound websocket connection"), + AST_CLI_DEFINE(ari_show_sessions, "Show websocket sessions"), + AST_CLI_DEFINE(ari_shut_session, "Shutdown websocket session"), + AST_CLI_DEFINE(ari_shut_sessions, "Shutdown websocket sessions"), }; -int ast_ari_cli_register(void) { +int ari_cli_register(void) { return ast_cli_register_multiple(cli_ari, ARRAY_LEN(cli_ari)); } -void ast_ari_cli_unregister(void) { +void ari_cli_unregister(void) { ast_cli_unregister_multiple(cli_ari, ARRAY_LEN(cli_ari)); } diff --git a/res/ari/config.c b/res/ari/config.c index 501487f519..558cf61cc7 100644 --- a/res/ari/config.c +++ b/res/ari/config.c @@ -22,38 +22,65 @@ * \author David M. Lee, II */ +#include + #include "asterisk.h" +#include "asterisk/sorcery.h" #include "asterisk/config_options.h" #include "asterisk/http_websocket.h" #include "asterisk/app.h" #include "asterisk/channel.h" +#include "asterisk/vector.h" #include "internal.h" -/*! \brief Locking container for safe configuration access. */ -static AO2_GLOBAL_OBJ_STATIC(confs); - -/*! \brief Mapping of the ARI conf struct's globals to the - * general context in the config file. */ -static struct aco_type general_option = { - .type = ACO_GLOBAL, - .name = "general", - .item_offset = offsetof(struct ast_ari_conf, general), - .category = "general", - .category_match = ACO_WHITELIST_EXACT, +static struct ast_sorcery *sorcery; + +struct outbound_websocket_state { + enum ari_conf_owc_fields invalid_fields; + char id[0]; }; -static struct aco_type *general_options[] = ACO_TYPES(&general_option); +#define OWC_STATES_BUCKETS 13 +struct ao2_container *owc_states = NULL; -/*! \brief Encoding format handler converts from boolean to enum. */ -static int encoding_format_handler(const struct aco_option *opt, +static void outbound_websocket_dtor(void *obj) +{ + struct ari_conf_outbound_websocket *owc = obj; + + ast_debug(3, "%s: Disposing of outbound websocket config\n", + ast_sorcery_object_get_id(owc)); + ast_string_field_free_memory(owc); +} + +static void *outbound_websocket_alloc(const char *id) +{ + struct ari_conf_outbound_websocket *owc = NULL; + + owc = ast_sorcery_generic_alloc(sizeof(*owc), outbound_websocket_dtor); + if (!owc) { + return NULL; + } + + if (ast_string_field_init(owc, 1024) != 0) { + ao2_cleanup(owc); + return NULL; + } + + ast_debug(2, "%s: Allocated outbound websocket config\n", id); + return owc; +} + +/*! \brief Parses the ast_ari_conf_outbound_ws_type enum from a config file */ +static int outbound_websocket_connection_type_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) { - struct ast_ari_conf_general *general = obj; + struct ari_conf_outbound_websocket *ows = obj; - if (!strcasecmp(var->name, "pretty")) { - general->format = ast_true(var->value) ? - AST_JSON_PRETTY : AST_JSON_COMPACT; + if (strcasecmp(var->value, "persistent") == 0) { + ows->connection_type = ARI_WS_TYPE_OUTBOUND_PERSISTENT; + } else if (strcasecmp(var->value, "per_call_config") == 0) { + ows->connection_type = ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG; } else { return -1; } @@ -61,16 +88,14 @@ static int encoding_format_handler(const struct aco_option *opt, return 0; } -/*! \brief Parses the ast_ari_password_format enum from a config file */ -static int password_format_handler(const struct aco_option *opt, - struct ast_variable *var, void *obj) +static int outbound_websocket_connection_type_to_str(const void *obj, const intptr_t *args, char **buf) { - struct ast_ari_conf_user *user = obj; + const struct ari_conf_outbound_websocket *owc = obj; - if (strcasecmp(var->value, "plain") == 0) { - user->password_format = ARI_PASSWORD_FORMAT_PLAIN; - } else if (strcasecmp(var->value, "crypt") == 0) { - user->password_format = ARI_PASSWORD_FORMAT_CRYPT; + if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) { + *buf = ast_strdup("persistent"); + } else if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG) { + *buf = ast_strdup("per_call_config"); } else { return -1; } @@ -78,183 +103,542 @@ static int password_format_handler(const struct aco_option *opt, return 0; } -/*! \brief Destructor for \ref ast_ari_conf_user */ -static void user_dtor(void *obj) +/* + * Can't use INT_MIN because it's an expression + * and macro substitutions using stringify can't + * handle that. + */ +#define DEFAULT_RECONNECT_ATTEMPTS -2147483648 + +/*! + * \brief Callback to initialize an outbound websocket object + * \retval 0 on success + * \retval CMP_MATCH on error which will cause the object to be removed + */ +static int outbound_websocket_apply(const struct ast_sorcery *sorcery, void *obj) { - struct ast_ari_conf_user *user = obj; - ast_debug(3, "Disposing of user %s\n", user->username); - ast_free(user->username); + struct ari_conf_outbound_websocket *owc = obj; + const char *id = ast_sorcery_object_get_id(owc); + int res = 0; + + ast_debug(3, "%s: Initializing outbound websocket\n", id); + + if (ast_strlen_zero(owc->uri)) { + ast_log(LOG_WARNING, "%s: Outbound websocket missing uri\n", id); + res = -1; + } + if (ast_strlen_zero(owc->protocols)) { + ast_log(LOG_WARNING, "%s: Outbound websocket missing protocols\n", id); + res = -1; + } + if (ast_strlen_zero(owc->apps)) { + ast_log(LOG_WARNING, "%s: Outbound websocket missing apps\n", id); + res = -1; + } else { + char *apps = ast_strdupa(owc->apps); + char *app; + while ((app = ast_strsep(&apps, ',', AST_STRSEP_STRIP))) { + + if (ast_strlen_zero(app)) { + ast_log(LOG_WARNING, "%s: Outbound websocket has empty app\n", id); + res = -1; + } + if (strlen(app) > ARI_MAX_APP_NAME_LEN) { + ast_log(LOG_WARNING, "%s: Outbound websocket app '%s' > %d characters\n", + id, app, (int)ARI_MAX_APP_NAME_LEN); + res = -1; + } + + } + } + + if (ast_strlen_zero(owc->local_ari_user)) { + ast_log(LOG_WARNING, "%s: Outbound websocket missing local_ari_user\n", id); + res = -1; + } + + if (res != 0) { + ast_log(LOG_WARNING, "%s: Outbound websocket configuration failed\n", id); + } else { + ast_debug(3, "%s: Outbound websocket configuration succeeded\n", id); + + if (owc->reconnect_attempts == DEFAULT_RECONNECT_ATTEMPTS) { + if (owc->connection_type == ARI_WS_TYPE_OUTBOUND_PERSISTENT) { + owc->reconnect_attempts = INT_MAX; + } else { + owc->reconnect_attempts = 4; + } + } + } + + /* Reminder: If res is -1, the config will be discarded. */ + return res; } -/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */ -static void *user_alloc(const char *cat) +enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id) { - RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); + RAII_VAR(struct outbound_websocket_state *, state, NULL, ao2_cleanup); - if (!cat) { - return NULL; + state = ao2_find(owc_states, id, OBJ_SEARCH_KEY); + return state ? state->invalid_fields : ARI_OWC_FIELD_NONE; +} + +static int outbound_websocket_validate_cb(void *obj, void *args, int flags) +{ + struct ari_conf_outbound_websocket *owc = obj; + struct ari_conf_outbound_websocket *other_owc = NULL; + RAII_VAR(struct ao2_container *, owcs, NULL, ao2_cleanup); + struct ao2_iterator it; + const char *id = ast_sorcery_object_get_id(owc); + struct ast_vector_string apps = { 0, }; + struct ari_conf_user *user = NULL; + struct outbound_websocket_state *state = NULL; + int res = 0; + + ast_debug(2, "%s: Validating outbound websocket\n", id); + + owcs = ari_conf_get_owcs(); + if (!owcs || ao2_container_count(owcs) == 0) { + return 0; + } + + if (AST_VECTOR_INIT(&apps, 5) != 0) { + return 0; } - ast_debug(3, "Allocating user %s\n", cat); + res = ast_vector_string_split(&apps, owc->apps, ",", 0, NULL); + if (res != 0) { + ast_log(LOG_WARNING, "%s: Outbound websocket apps '%s' failed to split\n", + id, owc->apps); + AST_VECTOR_RESET(&apps, ast_free_ptr); + AST_VECTOR_FREE(&apps); + return 0; + } - user = ao2_alloc_options(sizeof(*user), user_dtor, - AO2_ALLOC_OPT_LOCK_NOLOCK); + state = ao2_find(owc_states, id, OBJ_SEARCH_KEY); + if (!state) { + state = ao2_alloc(sizeof(*state) + strlen(id) + 1, NULL); + if (!state) { + ast_log(LOG_WARNING, "%s: Outbound websocket state allocation failed\n", id); + AST_VECTOR_RESET(&apps, ast_free_ptr); + AST_VECTOR_FREE(&apps); + return 0; + } + strcpy(state->id, id); /* Safe */ + ast_debug(3, "%s: Created new outbound websocket state\n", id); + } else { + ast_debug(3, "%s: Outbound websocket state already exists\n", id); + } + state->invalid_fields = ARI_OWC_FIELD_NONE; + + /* + * Check all other owcs to make sure we don't have + * duplicate apps. + */ + it = ao2_iterator_init(owcs, 0); + while ((other_owc = ao2_iterator_next(&it))) { + const char *other_id = ast_sorcery_object_get_id(other_owc); + if (!ast_strings_equal(other_id, id)) { + int i = 0; + for (i = 0; i < AST_VECTOR_SIZE(&apps); i++) { + const char *app = AST_VECTOR_GET(&apps, i); + if (ast_in_delimited_string(app, other_owc->apps, ',')) { + ast_log(LOG_WARNING, + "%s: Outbound websocket '%s' is also trying to register app '%s'\n", + id, other_id, app); + state->invalid_fields |= ARI_OWC_FIELD_APPS; + } + } + } + ao2_cleanup(other_owc); + if (owc->invalid) { + break; + } + } + ao2_iterator_destroy(&it); + AST_VECTOR_RESET(&apps, ast_free_ptr); + AST_VECTOR_FREE(&apps); + + /* + * Check that the local_ari_user is valid and has + * a plain text password. + */ + user = ast_sorcery_retrieve_by_id(sorcery, "user", owc->local_ari_user); if (!user) { + ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' not found\n", + id, owc->local_ari_user); + state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER; + } else { + if (user->password_format != ARI_PASSWORD_FORMAT_PLAIN) { + ast_log(LOG_WARNING, "%s: Outbound websocket ARI user '%s' password MUST be plain text\n", + id, owc->local_ari_user); + state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER; + } + if (ast_string_field_set(owc, local_ari_password, user->password) != 0) { + state->invalid_fields |= ARI_OWC_FIELD_LOCAL_ARI_USER; + } + } + ao2_cleanup(user); + + /* + * The container has AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE set so + * this is an insert or replace operation. + */ + ao2_link(owc_states, state); + ao2_cleanup(state); + + return 0; +} + +static int outbound_websocket_state_cleanup(void *obj, void *arg, int flags) +{ + struct outbound_websocket_state *state = obj; + struct ari_conf_outbound_websocket *owc = ari_conf_get_owc(state->id); + int res = 0; + + if (!owc) { + ast_debug(3, "%s: Cleaning up orphaned outbound websocket state\n", state->id); + res = CMP_MATCH; + } + ao2_cleanup(owc); + + return res; +} + +static void outbound_websockets_validate(const char *name) +{ + RAII_VAR(struct ao2_container *, owcs, ari_conf_get_owcs(), ao2_cleanup); + + ao2_callback(owcs, OBJ_NODATA, outbound_websocket_validate_cb, NULL); + /* Clean up any states whose configs have disappeared. */ + ao2_callback(owc_states, OBJ_NODATA | OBJ_UNLINK, + outbound_websocket_state_cleanup, NULL); +} + +struct ao2_container *ari_conf_get_owcs(void) +{ + if (!sorcery) { return NULL; } - user->username = ast_strdup(cat); - if (!user->username) { + return ast_sorcery_retrieve_by_fields(sorcery, "outbound_websocket", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); +} + +struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id) +{ + if (!sorcery) { return NULL; } - ao2_ref(user, +1); - return user; + return ast_sorcery_retrieve_by_id(sorcery, "outbound_websocket", id); } -/*! \brief Sorting function for use with red/black tree */ -static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags) +struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app( + const char *app_name, unsigned int ws_type) { - const struct ast_ari_conf_user *user_left = obj_left; - const struct ast_ari_conf_user *user_right = obj_right; - const char *key_right = obj_right; - int cmp; + struct ari_conf_outbound_websocket *owc = NULL; + struct ao2_container *owcs = NULL; + struct ao2_iterator i; - switch (flags & OBJ_SEARCH_MASK) { - case OBJ_SEARCH_OBJECT: - key_right = user_right->username; - /* Fall through */ - case OBJ_SEARCH_KEY: - cmp = strcasecmp(user_left->username, key_right); - break; - case OBJ_SEARCH_PARTIAL_KEY: - /* - * We could also use a partial key struct containing a length - * so strlen() does not get called for every comparison instead. - */ - cmp = strncasecmp(user_left->username, key_right, strlen(key_right)); - break; + if (ast_strlen_zero(app_name)) { + return NULL; + } + + ast_debug(3, "Checking outbound websockets for app '%s'\n", app_name); + + owcs = ari_conf_get_owcs(); + if (!owcs || ao2_container_count(owcs) == 0) { + ast_debug(3, "No outbound websockets found\n"); + return NULL; + } + + i = ao2_iterator_init(owcs, 0); + while ((owc = ao2_iterator_next(&i))) { + const char *id = ast_sorcery_object_get_id(owc); + + ast_debug(3, "%s: Checking outbound websocket apps '%s' for app '%s'\n", + id, owc->apps, app_name); + if (owc->connection_type & ws_type + && ast_in_delimited_string(app_name, owc->apps, ',')) { + ast_debug(3, "%s: Found correct websocket type for apps '%s' for app '%s'\n", + id, owc->apps, app_name); + break; + } + ao2_cleanup(owc); + } + ao2_iterator_destroy(&i); + ao2_cleanup(owcs); + if (!owc) { + ast_debug(3, "No outbound websocket found for app '%s'\n", app_name); + } + + return owc; +} + +const char *ari_websocket_type_to_str(enum ari_websocket_type type) +{ + switch (type) { + case ARI_WS_TYPE_OUTBOUND_PERSISTENT: + return "persistent"; + case ARI_WS_TYPE_OUTBOUND_PER_CALL: + return "per_call"; + case ARI_WS_TYPE_OUTBOUND_PER_CALL_CONFIG: + return "per_call_config"; + case ARI_WS_TYPE_INBOUND: + return "inbound"; + case ARI_WS_TYPE_ANY: + return "any"; default: - /* Sort can only work on something with a full or partial key. */ - ast_assert(0); - cmp = 0; - break; + return "unknown"; } - return cmp; } -/*! \brief \ref aco_type item_find function */ -static void *user_find(struct ao2_container *tmp_container, const char *cat) +enum ari_conf_owc_fields ari_conf_owc_detect_changes( + struct ari_conf_outbound_websocket *old_owc, + struct ari_conf_outbound_websocket *new_owc) { - if (!cat) { - return NULL; + enum ari_conf_owc_fields changed = ARI_OWC_FIELD_NONE; + const char *new_id = ast_sorcery_object_get_id(new_owc); + RAII_VAR(struct ast_variable *, changes, NULL, ast_variables_destroy); + struct ast_variable *v = NULL; + int res = 0; + + res = ast_sorcery_diff(sorcery, old_owc, new_owc, &changes); + if (res != 0) { + ast_log(LOG_WARNING, "%s: Failed to create changeset\n", new_id); + return ARI_OWC_FIELD_NONE; + } + + for (v = changes; v; v = v->next) { + ast_debug(2, "%s: %s changed to %s\n", new_id, v->name, v->value); + if (ast_strings_equal(v->name, "apps")) { + changed |= ARI_OWC_FIELD_APPS; + } else if (ast_strings_equal(v->name, "subscribe_all")) { + changed |= ARI_OWC_FIELD_SUBSCRIBE_ALL; + } else if (ast_strings_equal(v->name, "connection_type")) { + changed |= ARI_OWC_FIELD_CONNECTION_TYPE; + } else if (ast_strings_equal(v->name, "uri")) { + changed |= ARI_OWC_FIELD_URI; + } else if (ast_strings_equal(v->name, "protocols")) { + changed |= ARI_OWC_FIELD_PROTOCOLS; + } else if (ast_strings_equal(v->name, "username")) { + changed |= ARI_OWC_FIELD_USERNAME; + } else if (ast_strings_equal(v->name, "password")) { + changed |= ARI_OWC_FIELD_PASSWORD; + } else if (ast_strings_equal(v->name, "local_ari_user")) { + changed |= ARI_OWC_FIELD_LOCAL_ARI_USER; + } else if (ast_strings_equal(v->name, "local_ari_password")) { + changed |= ARI_OWC_FIELD_LOCAL_ARI_PASSWORD; + } else if (ast_strings_equal(v->name, "tls_enabled")) { + changed |= ARI_OWC_FIELD_TLS_ENABLED; + } else if (ast_strings_equal(v->name, "ca_list_file")) { + changed |= ARI_OWC_FIELD_CA_LIST_FILE; + } else if (ast_strings_equal(v->name, "ca_list_path")) { + changed |= ARI_OWC_FIELD_CA_LIST_PATH; + } else if (ast_strings_equal(v->name, "cert_file")) { + changed |= ARI_OWC_FIELD_CERT_FILE; + } else if (ast_strings_equal(v->name, "priv_key_file")) { + changed |= ARI_OWC_FIELD_PRIV_KEY_FILE; + } else if (ast_strings_equal(v->name, "reconnect_interval")) { + changed |= ARI_OWC_FIELD_RECONNECT_INTERVAL; + } else if (ast_strings_equal(v->name, "reconnect_attempts")) { + changed |= ARI_OWC_FIELD_RECONNECT_ATTEMPTS; + } else if (ast_strings_equal(v->name, "connection_timeout")) { + changed |= ARI_OWC_FIELD_CONNECTION_TIMEOUT; + } else if (ast_strings_equal(v->name, "verify_server_cert")) { + changed |= ARI_OWC_FIELD_VERIFY_SERVER_CERT; + } else if (ast_strings_equal(v->name, "verify_server_hostname")) { + changed |= ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME; + } else { + ast_debug(2, "%s: Unknown change %s\n", new_id, v->name); + } } - return ao2_find(tmp_container, cat, OBJ_SEARCH_KEY); + return changed; + } -static struct aco_type user_option = { - .type = ACO_ITEM, - .name = "user", - .category_match = ACO_BLACKLIST_EXACT, - .category = "general", - .matchfield = "type", - .matchvalue = "user", - .item_alloc = user_alloc, - .item_find = user_find, - .item_offset = offsetof(struct ast_ari_conf, users), -}; +/*! \brief \ref ast_ari_conf destructor. */ +static void general_dtor(void *obj) +{ + struct ari_conf_general *cfg = obj; -static struct aco_type *global_user[] = ACO_TYPES(&user_option); + ast_string_field_free_memory(cfg); +} -static void conf_general_dtor(void *obj) +static void *general_alloc(const char *name) { - struct ast_ari_conf_general *general = obj; + struct ari_conf_general *general = ast_sorcery_generic_alloc( + sizeof(*general), general_dtor); + + if (!general) { + return NULL; + } + + if (ast_string_field_init(general, 64) != 0) { + return NULL; + } - ast_string_field_free_memory(general); + return general; } -/*! \brief \ref ast_ari_conf destructor. */ -static void conf_destructor(void *obj) +#define MAX_VARS 128 + +static int general_apply(const struct ast_sorcery *sorcery, void *obj) { - struct ast_ari_conf *cfg = obj; + struct ari_conf_general *general = obj; + char *parse = NULL; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(vars)[MAX_VARS]; + ); - ao2_cleanup(cfg->general); - ao2_cleanup(cfg->users); + ast_debug(2, "Initializing general config\n"); + + parse = ast_strdupa(general->channelvars); + AST_STANDARD_APP_ARGS(args, parse); + + ast_channel_set_ari_vars(args.argc, args.vars); + return 0; } -/*! \brief Allocate an \ref ast_ari_conf for config parsing */ -static void *conf_alloc(void) +/*! \brief Encoding format handler converts from boolean to enum. */ +static int general_pretty_handler(const struct aco_option *opt, + struct ast_variable *var, void *obj) { - struct ast_ari_conf *cfg; + struct ari_conf_general *general = obj; - cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor, - AO2_ALLOC_OPT_LOCK_NOLOCK); - if (!cfg) { + general->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT; + + return 0; +} + +struct ari_conf_general* ari_conf_get_general(void) +{ + if (!sorcery) { return NULL; } - cfg->general = ao2_alloc_options(sizeof(*cfg->general), conf_general_dtor, - AO2_ALLOC_OPT_LOCK_NOLOCK); + return ast_sorcery_retrieve_by_id(sorcery, "general", "general"); +} - cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, - AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL); +static int general_pretty_to_str(const void *obj, const intptr_t *args, char **buf) +{ + const struct ari_conf_general *general = obj; + + if (general->format == AST_JSON_PRETTY) { + *buf = ast_strdup("yes"); + } else { + *buf = ast_strdup("no"); + } + return 0; +} - if (!cfg->users - || !cfg->general - || ast_string_field_init(cfg->general, 64) - || aco_set_defaults(&general_option, "general", cfg->general)) { - ao2_ref(cfg, -1); +/*! \brief Destructor for \ref ast_ari_conf_user */ +static void user_dtor(void *obj) +{ + struct ari_conf_user *user = obj; + ast_string_field_free_memory(user); + ast_debug(3, "%s: Disposing of user\n", ast_sorcery_object_get_id(user)); +} + +/*! \brief Allocate an \ref ast_ari_conf_user for config parsing */ +static void *user_alloc(const char *cat) +{ + struct ari_conf_user *user = ast_sorcery_generic_alloc( + sizeof(*user), user_dtor); + + if (!user) { return NULL; } - return cfg; + if (ast_string_field_init(user, 64) != 0) { + ao2_cleanup(user); + user = NULL; + } + + return user; } -#define CONF_FILENAME "ari.conf" +static int user_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct ari_conf_user *user = obj; + const char *id = ast_sorcery_object_get_id(user); -/*! \brief The conf file that's processed for the module. */ -static struct aco_file conf_file = { - /*! The config file name. */ - .filename = CONF_FILENAME, - /*! The mapping object types to be processed. */ - .types = ACO_TYPES(&general_option, &user_option), -}; + ast_debug(2, "%s: Initializing user\n", id); -CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc, - .files = ACO_FILES(&conf_file)); + if (ast_strlen_zero(user->password)) { + ast_log(LOG_WARNING, "%s: User missing password\n", id); + return -1; + } -struct ast_ari_conf *ast_ari_config_get(void) + return 0; +} + +/*! \brief Parses the ast_ari_password_format enum from a config file */ +static int user_password_format_handler(const struct aco_option *opt, + struct ast_variable *var, void *obj) { - struct ast_ari_conf *res = ao2_global_obj_ref(confs); - if (!res) { - ast_log(LOG_ERROR, - "Error obtaining config from " CONF_FILENAME "\n"); + struct ari_conf_user *user = obj; + + if (strcasecmp(var->value, "plain") == 0) { + user->password_format = ARI_PASSWORD_FORMAT_PLAIN; + } else if (strcasecmp(var->value, "crypt") == 0) { + user->password_format = ARI_PASSWORD_FORMAT_CRYPT; + } else { + return -1; } - return res; + + return 0; } -struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username, - const char *password) +static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf) { - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); - RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); - int is_valid = 0; + const struct ari_conf_user *user = obj; + + if (user->password_format == ARI_PASSWORD_FORMAT_CRYPT) { + *buf = ast_strdup("crypt"); + } else { + *buf = ast_strdup("plain"); + } + return 0; +} - conf = ast_ari_config_get(); - if (!conf) { +struct ao2_container *ari_conf_get_users(void) +{ + if (!sorcery) { return NULL; } - user = ao2_find(conf->users, username, OBJ_SEARCH_KEY); - if (!user) { + return ast_sorcery_retrieve_by_fields(sorcery, "user", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); +} + +struct ari_conf_user *ari_conf_get_user(const char *username) +{ + if (!sorcery) { return NULL; } - if (ast_strlen_zero(user->password)) { - ast_log(LOG_WARNING, - "User '%s' missing password; authentication failed\n", - user->username); + return ast_sorcery_retrieve_by_id(sorcery, "user", username); +} + +/* + * This is called by res_ari.c to validate the user and password + * for the websocket connection. + */ +struct ari_conf_user *ari_conf_validate_user(const char *username, + const char *password) +{ + struct ari_conf_user *user = NULL; + int is_valid = 0; + + if (ast_strlen_zero(username) || ast_strlen_zero(password)) { + return NULL; + } + + user = ast_sorcery_retrieve_by_id(sorcery, "user", username); + if (!user) { return NULL; } @@ -268,120 +652,211 @@ struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username, } if (!is_valid) { - return NULL; + ao2_cleanup(user); + user = NULL; } - ao2_ref(user, +1); return user; } -/*! \brief Callback to validate a user object */ -static int validate_user_cb(void *obj, void *arg, int flags) +int ari_sorcery_observer_add(const char *object_type, + const struct ast_sorcery_observer *callbacks) { - struct ast_ari_conf_user *user = obj; - - if (ast_strlen_zero(user->password)) { - ast_log(LOG_WARNING, "User '%s' missing password\n", - user->username); + if (!sorcery) { + return -1; } + return ast_sorcery_observer_add(sorcery, object_type, callbacks); +} +int ari_sorcery_observer_remove(const char *object_type, + const struct ast_sorcery_observer *callbacks) +{ + if (!sorcery) { + return -1; + } + ast_sorcery_observer_remove(sorcery, object_type, callbacks); return 0; } -/*! \brief Load (or reload) configuration. */ -static int process_config(int reload) +static void outbound_websocket_created_cb(const void *obj) { - RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); + const struct ari_conf_outbound_websocket *owc = obj; - switch (aco_process_config(&cfg_info, reload)) { - case ACO_PROCESS_ERROR: + ast_debug(3, "%s: Outbound XXXXXXXXXXXX websocket created\n", ast_sorcery_object_get_id(owc)); +} + +static struct ast_sorcery_observer observer_callbacks = { + .created = outbound_websocket_created_cb, + .loaded = outbound_websockets_validate, +}; + +#define ari_conf_bool(object, option, field, def_value) \ + ast_sorcery_object_field_register(sorcery, #object, #option, \ + def_value, OPT_YESNO_T, 1, \ + FLDSET(struct ari_conf_ ## object, field)) + +#define _stringify(val) #val +#define ari_conf_int(object, option, field, def_value) \ + ast_sorcery_object_field_register(sorcery, #object, #option, \ + _stringify(def_value), OPT_INT_T, PARSE_IN_RANGE, \ + FLDSET(struct ari_conf_ ## object, field), INT_MIN, INT_MAX) + +#define ari_conf_uint(object, option, field, def_value) \ + ast_sorcery_object_field_register(sorcery, #object, #option, \ + _stringify(def_value), OPT_UINT_T, PARSE_IN_RANGE, \ + FLDSET(struct ari_conf_ ## object, field), 0, UINT_MAX) + +#define ari_conf_sf(object, option, field, def_value) \ + ast_sorcery_object_field_register(sorcery, #object, #option, \ + def_value, OPT_STRINGFIELD_T, 0, \ + STRFLDSET(struct ari_conf_ ##object, field)) + +#define ari_conf_cust(object, option, def_value) \ + ast_sorcery_object_field_register_custom(sorcery, #object, #option, \ + def_value, object ## _ ## option ## _handler, \ + object ## _ ## option ## _to_str, NULL, 0, 0) + +AO2_STRING_FIELD_HASH_FN(outbound_websocket_state, id) +AO2_STRING_FIELD_CMP_FN(outbound_websocket_state, id) + +static int ari_conf_init(void) +{ + ast_debug(2, "Initializing ARI configuration\n"); + + owc_states = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, + AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, OWC_STATES_BUCKETS, + outbound_websocket_state_hash_fn, NULL, + outbound_websocket_state_cmp_fn); + if (!owc_states) { + ast_log(LOG_ERROR, "Failed to allocate outbound websocket states\n"); return -1; - case ACO_PROCESS_OK: - case ACO_PROCESS_UNCHANGED: - break; } - conf = ast_ari_config_get(); - if (!conf) { - ast_assert(0); /* We just configured; it should be there */ + if (!(sorcery = ast_sorcery_open())) { + ast_log(LOG_ERROR, "Failed to open sorcery\n"); return -1; } - if (conf->general->enabled) { - if (ao2_container_count(conf->users) == 0) { - ast_log(LOG_ERROR, "No configured users for ARI\n"); - } else { - ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL); - } + ast_sorcery_apply_default(sorcery, "general", "config", + "ari.conf,criteria=type=general,single_object=yes,explicit_name=general"); + ast_sorcery_apply_default(sorcery, "user", "config", + "ari.conf,criteria=type=user"); + ast_sorcery_apply_default(sorcery, "outbound_websocket", "config", + "ari.conf,criteria=type=outbound_websocket"); + + if (ast_sorcery_object_register(sorcery, "general", general_alloc, NULL, general_apply)) { + ast_log(LOG_ERROR, "Failed to register ARI general object with sorcery\n"); + ast_sorcery_unref(sorcery); + sorcery = NULL; + return -1; } - return 0; -} + if (ast_sorcery_object_register(sorcery, "user", user_alloc, NULL, user_apply)) { + ast_log(LOG_ERROR, "Failed to register ARI user object with sorcery\n"); + ast_sorcery_unref(sorcery); + sorcery = NULL; + return -1; + } -#define MAX_VARS 128 + if (ast_sorcery_object_register(sorcery, "outbound_websocket", outbound_websocket_alloc, + NULL, outbound_websocket_apply)) { + ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket object with sorcery\n"); + ast_sorcery_unref(sorcery); + sorcery = NULL; + return -1; + } -static int channelvars_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - char *parse = NULL; - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(vars)[MAX_VARS]; - ); + if (ast_sorcery_observer_add(sorcery, "outbound_websocket", &observer_callbacks)) { + ast_log(LOG_ERROR, "Failed to register ARI outbound_websocket observer with sorcery\n"); + ast_sorcery_unref(sorcery); + sorcery = NULL; + return -1; + } - parse = ast_strdupa(var->value); - AST_STANDARD_APP_ARGS(args, parse); + ast_sorcery_object_field_register_nodoc(sorcery, "general", "type", "", OPT_NOOP_T, 0, 0); + ari_conf_sf(general, auth_realm, auth_realm, "Asterisk REST Interface"); + ari_conf_sf(general, allowed_origins, allowed_origins, ""); + ari_conf_sf(general, channelvars, channelvars, ""); + ari_conf_bool(general, enabled, enabled, "yes"); + ari_conf_cust(general, pretty, "no"); + ari_conf_int(general, websocket_write_timeout, write_timeout, + AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT); + + + ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0); + ari_conf_sf(user, password, password, ""); + ari_conf_bool(user, read_only, read_only, "no"); + ari_conf_cust(user, password_format, "plain"); + + ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0); + ari_conf_sf(outbound_websocket, uri, uri, ""); + ari_conf_sf(outbound_websocket, protocols, protocols, ""); + ari_conf_sf(outbound_websocket, apps, apps, ""); + ari_conf_sf(outbound_websocket, username, username, ""); + ari_conf_sf(outbound_websocket, password, password, ""); + ari_conf_sf(outbound_websocket, local_ari_user, local_ari_user, ""); + ari_conf_sf(outbound_websocket, ca_list_file, ca_list_file, ""); + ari_conf_sf(outbound_websocket, ca_list_path, ca_list_path, ""); + ari_conf_sf(outbound_websocket, cert_file, cert_file, ""); + ari_conf_sf(outbound_websocket, priv_key_file, priv_key_file, ""); + ari_conf_bool(outbound_websocket, subscribe_all, subscribe_all, "no"); + ari_conf_bool(outbound_websocket, tls_enabled, tls_enabled, "no"); + ari_conf_bool(outbound_websocket, verify_server_cert, verify_server_cert, "yes"); + ari_conf_bool(outbound_websocket, verify_server_hostname, verify_server_hostname, "yes"); + ari_conf_cust(outbound_websocket, connection_type, ""); + ari_conf_int(outbound_websocket, connection_timeout, connect_timeout, 500); + ari_conf_int(outbound_websocket, reconnect_attempts, reconnect_attempts, DEFAULT_RECONNECT_ATTEMPTS); + ari_conf_int(outbound_websocket, reconnect_interval, reconnect_interval, 500); - ast_channel_set_ari_vars(args.argc, args.vars); return 0; } -int ast_ari_config_init(void) +int ari_conf_load(enum ari_conf_load_flags flags) { - if (aco_info_init(&cfg_info)) { - aco_info_destroy(&cfg_info); + void (*loader)(const struct ast_sorcery *sorcery, const char *type); + const char *msg_prefix; + + if (flags & ARI_CONF_RELOAD) { + loader = ast_sorcery_reload_object; + msg_prefix= "Reloading"; + } else { + loader = ast_sorcery_load_object; + msg_prefix= "Loading"; + } + + if (flags & ARI_CONF_INIT) { + if (ari_conf_init() != 0) { + ast_log(LOG_ERROR, "Failed to initialize ARI configuration\n"); + return -1; + } + } + + if (!sorcery) { + ast_log(LOG_ERROR, "ARI configuration not initialized\n"); return -1; } - /* ARI general category options */ - aco_option_register(&cfg_info, "enabled", ACO_EXACT, general_options, - "yes", OPT_BOOL_T, 1, - FLDSET(struct ast_ari_conf_general, enabled)); - aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT, - general_options, "no", encoding_format_handler, 0); - aco_option_register(&cfg_info, "auth_realm", ACO_EXACT, general_options, - "Asterisk REST Interface", OPT_CHAR_ARRAY_T, 0, - FLDSET(struct ast_ari_conf_general, auth_realm), - ARI_AUTH_REALM_LEN); - 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_custom(&cfg_info, "channelvars", ACO_EXACT, general_options, - "", channelvars_handler, 0); - - /* ARI type=user category options */ - aco_option_register(&cfg_info, "type", ACO_EXACT, global_user, NULL, - OPT_NOOP_T, 0, 0); - aco_option_register(&cfg_info, "read_only", ACO_EXACT, global_user, - "no", OPT_BOOL_T, 1, - FLDSET(struct ast_ari_conf_user, read_only)); - aco_option_register(&cfg_info, "password", ACO_EXACT, global_user, - "", OPT_CHAR_ARRAY_T, 0, - FLDSET(struct ast_ari_conf_user, password), ARI_PASSWORD_LEN); - aco_option_register_custom(&cfg_info, "password_format", ACO_EXACT, - global_user, "plain", password_format_handler, 0); - - return process_config(0); -} - -int ast_ari_config_reload(void) -{ - return process_config(1); -} - -void ast_ari_config_destroy(void) -{ - aco_info_destroy(&cfg_info); - ao2_global_obj_release(confs); + if (flags & ARI_CONF_LOAD_GENERAL) { + ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "general"); + loader(sorcery, "general"); + } + + if (flags & ARI_CONF_LOAD_USER) { + ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "user"); + loader(sorcery, "user"); + } + + if (flags & ARI_CONF_LOAD_OWC) { + ast_debug(2, "%s ARI '%s' configuration\n", msg_prefix, "outbound_websocket"); + loader(sorcery, "outbound_websocket"); + } + + return 0; +} + +void ari_conf_destroy(void) +{ + ast_sorcery_unref(sorcery); + sorcery = NULL; + ao2_cleanup(owc_states); } diff --git a/res/ari/internal.h b/res/ari/internal.h index 08a633894a..4829bad2bb 100644 --- a/res/ari/internal.h +++ b/res/ari/internal.h @@ -27,7 +27,11 @@ #include "asterisk/http.h" #include "asterisk/json.h" +#include "asterisk/md5.h" +#include "asterisk/sorcery.h" #include "asterisk/stringfields.h" +#include "ari_websockets.h" + /*! @{ */ @@ -37,98 +41,172 @@ * \return 0 on success. * \return Non-zero on error. */ -int ast_ari_cli_register(void); +int ari_cli_register(void); /*! * \brief Unregister CLI commands for ARI. */ -void ast_ari_cli_unregister(void); +void ari_cli_unregister(void); /*! @} */ /*! @{ */ -struct ast_ari_conf_general; - -/*! \brief All configuration options for ARI. */ -struct ast_ari_conf { - /*! The general section configuration options. */ - struct ast_ari_conf_general *general; - /*! Configured users */ - struct ao2_container *users; -}; - -/*! Max length for auth_realm field */ -#define ARI_AUTH_REALM_LEN 256 - /*! \brief Global configuration options for ARI. */ -struct ast_ari_conf_general { +struct ari_conf_general { + SORCERY_OBJECT(details); + AST_DECLARE_STRING_FIELDS( + /*! Allowed CORS origins */ + AST_STRING_FIELD(allowed_origins); + /*! Authentication realm */ + AST_STRING_FIELD(auth_realm); + /*! Channel variables */ + AST_STRING_FIELD(channelvars); + ); /*! 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 */ - char auth_realm[ARI_AUTH_REALM_LEN]; - - AST_DECLARE_STRING_FIELDS( - AST_STRING_FIELD(allowed_origins); - ); }; /*! \brief Password format */ -enum ast_ari_password_format { +enum ari_user_password_format { /*! \brief Plaintext password */ ARI_PASSWORD_FORMAT_PLAIN, /*! crypt(3) password */ ARI_PASSWORD_FORMAT_CRYPT, }; -/*! - * \brief User's password mx length. - * - * If 256 seems like a lot, a crypt SHA-512 has over 106 characters. - */ -#define ARI_PASSWORD_LEN 256 - /*! \brief Per-user configuration options */ -struct ast_ari_conf_user { - /*! Username for authentication */ - char *username; - /*! User's password. */ - char password[ARI_PASSWORD_LEN]; +struct ari_conf_user { + SORCERY_OBJECT(details); + AST_DECLARE_STRING_FIELDS( + /*! User's password. */ + AST_STRING_FIELD(password); + ); /*! Format for the password field */ - enum ast_ari_password_format password_format; + enum ari_user_password_format password_format; /*! If true, user cannot execute change operations */ int read_only; }; +enum ari_conf_owc_fields { + ARI_OWC_FIELD_NONE = 0, + ARI_OWC_FIELD_URI = (1 << 0), + ARI_OWC_FIELD_PROTOCOLS = (1 << 1), + ARI_OWC_FIELD_APPS = (1 << 2), + ARI_OWC_FIELD_USERNAME = (1 << 3), + ARI_OWC_FIELD_PASSWORD = (1 << 4), + ARI_OWC_FIELD_LOCAL_ARI_USER = (1 << 5), + ARI_OWC_FIELD_LOCAL_ARI_PASSWORD = (1 << 6), + ARI_OWC_FIELD_TLS_ENABLED = (1 << 7), + ARI_OWC_FIELD_CA_LIST_FILE = (1 << 8), + ARI_OWC_FIELD_CA_LIST_PATH = (1 << 9), + ARI_OWC_FIELD_CERT_FILE = (1 << 10), + ARI_OWC_FIELD_PRIV_KEY_FILE = (1 << 11), + ARI_OWC_FIELD_SUBSCRIBE_ALL = (1 << 12), + ARI_OWC_FIELD_CONNECTION_TYPE = (1 << 13), + ARI_OWC_FIELD_RECONNECT_INTERVAL = (1 << 14), + ARI_OWC_FIELD_RECONNECT_ATTEMPTS = (1 << 15), + ARI_OWC_FIELD_CONNECTION_TIMEOUT = (1 << 16), + ARI_OWC_FIELD_VERIFY_SERVER_CERT = (1 << 17), + ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME = (1 << 18), + ARI_OWC_NEEDS_RECONNECT = ARI_OWC_FIELD_URI | ARI_OWC_FIELD_PROTOCOLS + | ARI_OWC_FIELD_CONNECTION_TYPE + | ARI_OWC_FIELD_USERNAME | ARI_OWC_FIELD_PASSWORD + | ARI_OWC_FIELD_LOCAL_ARI_USER | ARI_OWC_FIELD_LOCAL_ARI_PASSWORD + | ARI_OWC_FIELD_TLS_ENABLED | ARI_OWC_FIELD_CA_LIST_FILE + | ARI_OWC_FIELD_CA_LIST_PATH | ARI_OWC_FIELD_CERT_FILE + | ARI_OWC_FIELD_PRIV_KEY_FILE | ARI_OWC_FIELD_VERIFY_SERVER_CERT + | ARI_OWC_FIELD_VERIFY_SERVER_HOSTNAME, + ARI_OWC_NEEDS_REREGISTER = ARI_OWC_FIELD_APPS | ARI_OWC_FIELD_SUBSCRIBE_ALL, +}; + +struct ari_conf_outbound_websocket { + SORCERY_OBJECT(details); + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(uri); /*!< Server URI */ + AST_STRING_FIELD(protocols); /*!< Websocket protocols to use with server */ + AST_STRING_FIELD(apps); /*!< Stasis apps using this connection */ + AST_STRING_FIELD(username); /*!< Auth user name */ + AST_STRING_FIELD(password); /*!< Auth password */ + AST_STRING_FIELD(local_ari_user);/*!< The ARI user to act as */ + AST_STRING_FIELD(local_ari_password);/*!< The password for the ARI user */ + AST_STRING_FIELD(ca_list_file); /*!< CA file */ + AST_STRING_FIELD(ca_list_path); /*!< CA path */ + AST_STRING_FIELD(cert_file); /*!< Certificate file */ + AST_STRING_FIELD(priv_key_file); /*!< Private key file */ + ); + int invalid; /*!< Invalid configuration */ + enum ari_conf_owc_fields invalid_fields; /*!< Invalid fields */ + enum ari_websocket_type connection_type; /*!< Connection type */ + int connect_timeout; /*!< Connection timeout (ms) */ + int subscribe_all; /*!< Subscribe to all events */ + unsigned int reconnect_attempts; /*!< How many attempts before returning an error */ + unsigned int reconnect_interval; /*!< How often to attempt a reconnect (ms) */ + int tls_enabled; /*!< TLS enabled */ + int verify_server_cert; /*!< Verify server certificate */ + int verify_server_hostname; /*!< Verify server hostname */ +}; + /*! - * \brief Initialize the ARI configuration + * \brief Detect changes between two outbound websocket configurations. + * + * \param old_owc The old outbound websocket configuration. + * \param new_owc The new outbound websocket configuration. + * \return A bitmask of changed fields. */ -int ast_ari_config_init(void); +enum ari_conf_owc_fields ari_conf_owc_detect_changes( + struct ari_conf_outbound_websocket *old_owc, + struct ari_conf_outbound_websocket *new_owc); /*! - * \brief Reload the ARI configuration + * \brief Get the outbound websocket configuration for a Stasis app. + * + * \param app_name The application name to search for. + * \param ws_type An OR'd list of ari_websocket_types or ARI_WS_TYPE_ANY. + * + * \retval ARI outbound websocket configuration object. + * \retval NULL if not found. */ -int ast_ari_config_reload(void); +struct ari_conf_outbound_websocket *ari_conf_get_owc_for_app( + const char *app_name, unsigned int ws_type); + +enum ari_conf_load_flags { + ARI_CONF_INIT = (1 << 0), /*!< Initialize sorcery */ + ARI_CONF_RELOAD = (1 << 1), /*!< Reload sorcery */ + ARI_CONF_LOAD_GENERAL = (1 << 2), /*!< Load general config */ + ARI_CONF_LOAD_USER = (1 << 3), /*!< Load user config */ + ARI_CONF_LOAD_OWC = (1 << 4), /*!< Load outbound websocket config */ + ARI_CONF_LOAD_ALL = ( /*!< Load all configs */ + ARI_CONF_LOAD_GENERAL + | ARI_CONF_LOAD_USER + | ARI_CONF_LOAD_OWC), +}; /*! - * \brief Destroy the ARI configuration + * \brief (Re)load the ARI configuration */ -void ast_ari_config_destroy(void); +int ari_conf_load(enum ari_conf_load_flags flags); /*! - * \brief Get the current ARI configuration. - * - * This is an immutable object, so don't modify it. It is AO2 managed, so - * ao2_cleanup() when you're done with it. - * - * \return ARI configuration object. - * \retval NULL on error. + * \brief Destroy the ARI configuration */ -struct ast_ari_conf *ast_ari_config_get(void); +void ari_conf_destroy(void); + +struct ari_conf_general* ari_conf_get_general(void); +struct ao2_container *ari_conf_get_users(void); +struct ari_conf_user *ari_conf_get_user(const char *username); +struct ao2_container *ari_conf_get_owcs(void); +struct ari_conf_outbound_websocket *ari_conf_get_owc(const char *id); +enum ari_conf_owc_fields ari_conf_owc_get_invalid_fields(const char *id); +const char *ari_websocket_type_to_str(enum ari_websocket_type type); +int ari_sorcery_observer_add(const char *object_type, + const struct ast_sorcery_observer *callbacks); +int ari_sorcery_observer_remove(const char *object_type, + const struct ast_sorcery_observer *callbacks); /*! * \brief Validated a user's credentials. @@ -138,7 +216,7 @@ struct ast_ari_conf *ast_ari_config_get(void); * \return User object. * \retval NULL if username or password is invalid. */ -struct ast_ari_conf_user *ast_ari_config_validate_user(const char *username, +struct ari_conf_user *ari_conf_validate_user(const char *username, const char *password); /*! @} */ diff --git a/res/res_ari.c b/res/res_ari.c index 56d8c28154..50d0f1730b 100644 --- a/res/res_ari.c +++ b/res/res_ari.c @@ -77,114 +77,6 @@ core ***/ -/*** DOCUMENTATION - - HTTP binding for the Stasis API - - - - 12.0.0 - - General configuration settings - - - 12.0.0 - - Enable/disable the ARI module - - This option enables or disables the ARI module. - - ARI uses Asterisk's HTTP server, which must also be enabled in http.conf. - - - - http.conf - https://docs.asterisk.org/Configuration/Core-Configuration/Asterisk-Builtin-mini-HTTP-Server/ - - - - - 11.11.0 - 12.4.0 - - The timeout (in milliseconds) to set on WebSocket connections. - - 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. - - - - - 12.0.0 - - Responses from ARI are formatted to be human readable - - - - 12.0.0 - - Realm to use for authentication. Defaults to Asterisk REST Interface. - - - - 12.0.0 - - Comma separated list of allowed origins, for Cross-Origin Resource Sharing. May be set to * to allow all origins. - - - - 14.2.0 - - Comma separated list of channel variables to display in channel json. - - - - - - 12.0.0 - - Per-user configuration settings - - - 13.30.0 - 16.7.0 - 17.1.0 - - Define this configuration section as a user. - - - Configure this section as a user - - - - - - 13.30.0 - 16.7.0 - 17.1.0 - - When set to yes, user is only authorized for read-only requests - - - - 13.30.0 - 16.7.0 - 17.1.0 - - Crypted or plaintext password (see password_format) - - - - 12.0.0 - - 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 using mkpasswd -m sha-512. When set to plain, the password is in plaintext - - - - -***/ - #include "asterisk.h" #include "ari/internal.h" @@ -202,8 +94,8 @@ /*! \brief Helper function to check if module is enabled. */ static int is_enabled(void) { - RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup); - return cfg && cfg->general && cfg->general->enabled; + RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); + return general && general->enabled; } /*! Lock for \ref root_handler */ @@ -389,9 +281,9 @@ static void add_allow_header(struct stasis_rest_handlers *handler, static int origin_allowed(const char *origin) { - RAII_VAR(struct ast_ari_conf *, cfg, ast_ari_config_get(), ao2_cleanup); + RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); - char *allowed = ast_strdupa(cfg->general->allowed_origins); + char *allowed = ast_strdupa(general ? general->allowed_origins : ""); char *current; while ((current = strsep(&allowed, ","))) { @@ -555,7 +447,7 @@ static void handle_options(struct stasis_rest_handlers *handler, * \return User object for the authenticated user. * \retval NULL if authentication failed. */ -static struct ast_ari_conf_user *authenticate_api_key(const char *api_key) +static struct ari_conf_user *authenticate_api_key(const char *api_key) { RAII_VAR(char *, copy, NULL, ast_free); char *username; @@ -572,7 +464,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key) return NULL; } - return ast_ari_config_validate_user(username, password); + return ari_conf_validate_user(username, password); } /*! @@ -583,7 +475,7 @@ static struct ast_ari_conf_user *authenticate_api_key(const char *api_key) * \return User object for the authenticated user. * \retval NULL if authentication failed. */ -static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_params, +static struct ari_conf_user *authenticate_user(struct ast_variable *get_params, struct ast_variable *headers) { RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup); @@ -592,7 +484,7 @@ static struct ast_ari_conf_user *authenticate_user(struct ast_variable *get_para /* HTTP Basic authentication */ http_auth = ast_http_get_auth(headers); if (http_auth) { - return ast_ari_config_validate_user(http_auth->userid, + return ari_conf_validate_user(http_auth->userid, http_auth->password); } @@ -642,8 +534,8 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se struct stasis_rest_handlers *handler = NULL; struct stasis_rest_handlers *wildcard_handler = NULL; RAII_VAR(struct ast_variable *, path_vars, NULL, ast_variables_destroy); - RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); - RAII_VAR(struct ast_ari_conf *, conf, ast_ari_config_get(), ao2_cleanup); + RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); + RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); char *path = ast_strdupa(uri); char *path_segment = NULL; @@ -651,7 +543,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se SCOPE_ENTER(3, "Request: %s %s, path:%s\n", ast_get_http_method(method), uri, path); - if (!conf || !conf->general) { + if (!general) { if (ser && source == ARI_INVOKE_SOURCE_REST) { ast_http_request_close_on_completion(ser); } @@ -679,7 +571,7 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se */ ast_str_append(&response->headers, 0, "WWW-Authenticate: Basic realm=\"%s\"\r\n", - conf->general->auth_realm); + general->auth_realm); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } else if (!ast_fully_booted) { @@ -1014,9 +906,8 @@ static void process_cors_request(struct ast_variable *headers, enum ast_json_encoding_format ast_ari_json_format(void) { - RAII_VAR(struct ast_ari_conf *, cfg, NULL, ao2_cleanup); - cfg = ast_ari_config_get(); - return cfg->general->format; + RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); + return general ? general->format : AST_JSON_COMPACT; } /*! @@ -1230,14 +1121,14 @@ static int unload_module(void) { ari_websocket_unload_module(); - ast_ari_cli_unregister(); + ari_cli_unregister(); if (is_enabled()) { ast_debug(3, "Disabling ARI\n"); ast_http_uri_unlink(&http_uri); } - ast_ari_config_destroy(); + ari_conf_destroy(); ao2_cleanup(root_handler); root_handler = NULL; @@ -1272,12 +1163,33 @@ static int load_module(void) return AST_MODULE_LOAD_DECLINE; } - if (ast_ari_config_init() != 0) { + /* + * ari_websocket_load_module() needs to know if ARI is enabled + * globally so it needs the "general" config to be loaded but it + * also needs to register a sorcery object observer for + * "outbound_websocket" BEFORE the outbound_websocket configs are loaded. + * outbound_websocket in turn needs the users to be loaded so we'll + * initialize sorcery and load "general" and "user" configs first, then + * load the websocket module, then load the "outbound_websocket" configs + * which will fire the observers. + */ + if (ari_conf_load(ARI_CONF_INIT | ARI_CONF_LOAD_GENERAL | ARI_CONF_LOAD_USER) != 0) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + if (ari_websocket_load_module(is_enabled()) != AST_MODULE_LOAD_SUCCESS) { unload_module(); return AST_MODULE_LOAD_DECLINE; } - if (ari_websocket_load_module() != AST_MODULE_LOAD_SUCCESS) { + /* + * Now we can load the outbound_websocket configs which will + * fire the observers. + */ + ari_conf_load(ARI_CONF_LOAD_OWC); + + if (ari_cli_register() != 0) { unload_module(); return AST_MODULE_LOAD_DECLINE; } @@ -1289,26 +1201,22 @@ static int load_module(void) ast_debug(3, "ARI disabled\n"); } - if (ast_ari_cli_register() != 0) { - unload_module(); - return AST_MODULE_LOAD_DECLINE; - } - return AST_MODULE_LOAD_SUCCESS; } static int reload_module(void) { char was_enabled = is_enabled(); + int is_now_enabled = 0; - if (ast_ari_config_reload() != 0) { - return AST_MODULE_LOAD_DECLINE; - } + ari_conf_load(ARI_CONF_RELOAD | ARI_CONF_LOAD_ALL); + + is_now_enabled = is_enabled(); - if (was_enabled && !is_enabled()) { + if (was_enabled && !is_now_enabled) { ast_debug(3, "Disabling ARI\n"); ast_http_uri_unlink(&http_uri); - } else if (!was_enabled && is_enabled()) { + } else if (!was_enabled && is_now_enabled) { ast_debug(3, "Enabling ARI\n"); ast_http_uri_link(&http_uri); } diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json index ef0849fc63..fd300981b6 100644 --- a/rest-api/api-docs/events.json +++ b/rest-api/api-docs/events.json @@ -169,6 +169,8 @@ "RecordingFinished", "RecordingFailed", "ApplicationMoveFailed", + "ApplicationRegistered", + "ApplicationUnregistered", "ApplicationReplaced", "BridgeCreated", "BridgeDestroyed", @@ -367,6 +369,16 @@ } } }, + "ApplicationRegistered": { + "id": "ApplicationRegistered", + "description": "Notification that a Stasis app has been registered.", + "properties": {} + }, + "ApplicationUnregistered": { + "id": "ApplicationUnregistered", + "description": "Notification that a Stasis app has been unregistered.", + "properties": {} + }, "ApplicationReplaced": { "id": "ApplicationReplaced", "description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",