res/ari: Register Stasis application on WebSocket attempt
authorMatt Jordan <mjordan@digium.com>
Mon, 18 May 2015 01:36:41 +0000 (20:36 -0500)
committerMatt Jordan <mjordan@digium.com>
Fri, 22 May 2015 16:13:34 +0000 (11:13 -0500)
Prior to this patch, when a WebSocket connection is made, ARI would not
be informed of the connection until after the WebSocket layer had
accepted the connection. This created a brief race condition where the
ARI client would be notified that it was connected, a channel would be
sent into the Stasis dialplan application, but ARI would not yet have
registered the Stasis application presented in the HTTP request that
established the WebSocket.

This patch resolves this issue by doing the following:
 * When a WebSocket attempt is made, a callback is made into the ARI
   application layer, which verifies and registers the apps presented in
   the HTTP request. Because we do not yet have a WebSocket, we cannot
   have an event session for the corresponding applications. Some
   defensive checks were thus added to make the application objects
   tolerant to a NULL event session.
 * When a WebSocket connection is made, the registered application is
   updated with the newly created event session that wraps the WebSocket
   connection.

ASTERISK-24988 #close
Reported by: Joshua Colp

Change-Id: Ia5dc60dc2b6bee76cd5aff0f69dd53b36e83f636

res/ari/resource_events.c
res/ari/resource_events.h
res/res_ari_events.c
res/stasis/app.c
rest-api-templates/ari_resource.h.mustache
rest-api-templates/res_ari_resource.c.mustache

index eddcb66..e666f2e 100644 (file)
@@ -119,6 +119,10 @@ static void app_handler(void *data, const char *app_name,
        const char *msg_application = S_OR(
                ast_json_string_get(ast_json_object_get(message, "application")),
                "");
+
+       if (!session) {
+               return;
+       }
  
        /* Determine if we've been replaced */
        if (strcmp(msg_type, "ApplicationReplaced") == 0 &&
@@ -168,7 +172,40 @@ static int session_register_app(struct event_session *session,
        return 0;
 }
 
-void ast_ari_websocket_events_event_websocket(struct ast_ari_websocket_session *ws_session,
+int ast_ari_websocket_events_event_websocket_attempted(struct ast_tcptls_session_instance *ser,
+       struct ast_variable *headers,
+       struct ast_ari_events_event_websocket_args *args)
+{
+       int res = 0;
+       size_t i, j;
+
+       ast_debug(3, "/events WebSocket attempted\n");
+
+       if (args->app_count == 0) {
+               ast_http_error(ser, 400, "Bad Request", "Missing param 'app'");
+               return -1;
+       }
+
+       for (i = 0; i < args->app_count; ++i) {
+               if (ast_strlen_zero(args->app[i])) {
+                       res = -1;
+                       break;
+               }
+
+               res |= stasis_app_register(args->app[i], app_handler, NULL);
+       }
+
+       if (res) {
+               for (j = 0; j < i; ++j) {
+                       stasis_app_unregister(args->app[j]);
+               }
+               ast_http_error(ser, 400, "Bad Request", "Invalid application provided in param 'app'.");
+       }
+
+       return res;
+}
+
+void ast_ari_websocket_events_event_websocket_established(struct ast_ari_websocket_session *ws_session,
        struct ast_variable *headers,
        struct ast_ari_events_event_websocket_args *args)
 {
index 646cf9b..2b63181 100644 (file)
@@ -48,6 +48,19 @@ struct ast_ari_events_event_websocket_args {
        /*! Parsing context for app. */
        char *app_parse;
 };
+
+/*!
+ * \brief WebSocket connection for events.
+ *
+ * \param ser HTTP TCP/TLS Server Session
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ *
+ * \retval 0 success
+ * \retval non-zero error
+ */
+int ast_ari_websocket_events_event_websocket_attempted(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_events_event_websocket_args *args);
+
 /*!
  * \brief WebSocket connection for events.
  *
@@ -55,7 +68,7 @@ struct ast_ari_events_event_websocket_args {
  * \param headers HTTP headers.
  * \param args Swagger parameters.
  */
-void ast_ari_websocket_events_event_websocket(struct ast_ari_websocket_session *session, struct ast_variable *headers, struct ast_ari_events_event_websocket_args *args);
+void ast_ari_websocket_events_event_websocket_established(struct ast_ari_websocket_session *session, struct ast_variable *headers, struct ast_ari_events_event_websocket_args *args);
 /*! Argument struct for ast_ari_events_user_event() */
 struct ast_ari_events_user_event_args {
        /*! Event name */
index 66b01a7..4542339 100644 (file)
@@ -53,7 +53,92 @@ ASTERISK_REGISTER_FILE()
 
 #define MAX_VALS 128
 
-static void ast_ari_events_event_websocket_ws_cb(struct ast_websocket *ws_session,
+static int ast_ari_events_event_websocket_ws_attempted_cb(struct ast_tcptls_session_instance *ser, struct ast_variable *get_params, struct ast_variable *headers)
+{
+       struct ast_ari_events_event_websocket_args args = {};
+       int res = 0;
+       RAII_VAR(struct ast_ari_response *, response, NULL, ast_free);
+       struct ast_variable *i;
+
+       response = ast_calloc(1, sizeof(*response));
+       if (!response) {
+               ast_log(LOG_ERROR, "Failed to create response.\n");
+               goto fin;
+       }
+
+       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;
+
+                       args.app_parse = ast_strdup(i->value);
+                       if (!args.app_parse) {
+                               ast_ari_response_alloc_failed(response);
+                               goto fin;
+                       }
+
+                       if (strlen(args.app_parse) == 0) {
+                               /* ast_app_separate_args can't handle "" */
+                               args.app_count = 1;
+                               vals[0] = args.app_parse;
+                       } else {
+                               args.app_count = ast_app_separate_args(
+                                       args.app_parse, ',', vals,
+                                       ARRAY_LEN(vals));
+                       }
+
+                       if (args.app_count == 0) {
+                               ast_ari_response_alloc_failed(response);
+                               goto fin;
+                       }
+
+                       if (args.app_count >= MAX_VALS) {
+                               ast_ari_response_error(response, 400,
+                                       "Bad Request",
+                                       "Too many values for app");
+                               goto fin;
+                       }
+
+                       args.app = ast_malloc(sizeof(*args.app) * args.app_count);
+                       if (!args.app) {
+                               ast_ari_response_alloc_failed(response);
+                               goto fin;
+                       }
+
+                       for (j = 0; j < args.app_count; ++j) {
+                               args.app[j] = (vals[j]);
+                       }
+               } else
+               {}
+       }
+
+       res = ast_ari_websocket_events_event_websocket_attempted(ser, headers, &args);
+
+fin: __attribute__((unused))
+       if (!response) {
+               ast_http_error(ser, 500, "Server Error", "Memory allocation error");
+               res = -1;
+       } else if (response->response_code != 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);
+               }
+               res = -1;
+       }
+       ast_free(args.app_parse);
+       ast_free(args.app);
+       return res;
+}
+
+static void ast_ari_events_event_websocket_ws_established_cb(struct ast_websocket *ws_session,
        struct ast_variable *get_params, struct ast_variable *headers)
 {
        struct ast_ari_events_event_websocket_args args = {};
@@ -126,16 +211,11 @@ static void ast_ari_events_event_websocket_ws_cb(struct ast_websocket *ws_sessio
                {}
        }
 
-       ast_ari_websocket_events_event_websocket(session, headers, &args);
+       ast_ari_websocket_events_event_websocket_established(session, headers, &args);
 
 fin: __attribute__((unused))
        if (response && response->response_code != 0) {
                /* Param parsing failure */
-               /* TODO - ideally, this would return the error code to the
-                * HTTP client; but we've already done the WebSocket
-                * negotiation. Param parsing should happen earlier, but we
-                * need a way to pass it through the WebSocket code to the
-                * callback */
                RAII_VAR(char *, msg, NULL, ast_json_free);
                if (response->message) {
                        msg = ast_json_dump_string(response->message);
@@ -351,12 +431,22 @@ static struct stasis_rest_handlers events = {
 static int load_module(void)
 {
        int res = 0;
+       struct ast_websocket_protocol *protocol;
+
        events.ws_server = ast_websocket_server_create();
        if (!events.ws_server) {
                return AST_MODULE_LOAD_FAILURE;
        }
-       res |= ast_websocket_server_add_protocol(events.ws_server,
-               "ari", ast_ari_events_event_websocket_ws_cb);
+
+       protocol = ast_websocket_sub_protocol_alloc("ari");
+       if (!protocol) {
+               ao2_ref(events.ws_server, -1);
+               events.ws_server = NULL;
+               return AST_MODULE_LOAD_FAILURE;
+       }
+       protocol->session_attempted = ast_ari_events_event_websocket_ws_attempted_cb;
+       protocol->session_established = ast_ari_events_event_websocket_ws_established_cb;
+       res |= ast_websocket_server_add_protocol2(events.ws_server, protocol);
        stasis_app_ref();
        res |= ast_ari_add_handler(&events);
        return res;
index 5a1c5f8..b99e232 100644 (file)
@@ -871,8 +871,7 @@ struct stasis_app *app_create(const char *name, stasis_app_cb handler, void *dat
 
        strncpy(app->name, name, size - sizeof(*app));
        app->handler = handler;
-       ao2_ref(data, +1);
-       app->data = data;
+       app->data = ao2_bump(data);
 
        ao2_ref(app, +1);
        return app;
@@ -950,7 +949,7 @@ void app_update(struct stasis_app *app, stasis_app_cb handler, void *data)
 {
        SCOPED_AO2LOCK(lock, app);
 
-       if (app->handler) {
+       if (app->handler && app->data) {
                RAII_VAR(struct ast_json *, msg, NULL, ast_json_unref);
 
                ast_verb(1, "Replacing Stasis app '%s'\n", app->name);
index 3a20776..d3f40b6 100644 (file)
@@ -89,6 +89,23 @@ int ast_ari_{{c_name}}_{{c_nickname}}_parse_body(
 void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args, struct ast_ari_response *response);
 {{/is_req}}
 {{#is_websocket}}
+
+/*!
+ * \brief {{summary}}
+{{#notes}}
+ *
+ * {{{notes}}}
+{{/notes}}
+ *
+ * \param ser HTTP TCP/TLS Server Session
+ * \param headers HTTP headers
+ * \param args Swagger parameters
+ *
+ * \retval 0 success
+ * \retval non-zero error
+ */
+int ast_ari_websocket_{{c_name}}_{{c_nickname}}_attempted(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args);
+
 /*!
  * \brief {{summary}}
 {{#notes}}
@@ -100,7 +117,7 @@ void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_variable *headers, struct ast_
  * \param headers HTTP headers.
  * \param args Swagger parameters.
  */
-void ast_ari_websocket_{{c_name}}_{{c_nickname}}(struct ast_ari_websocket_session *session, struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args);
+void ast_ari_websocket_{{c_name}}_{{c_nickname}}_established(struct ast_ari_websocket_session *session, struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args);
 {{/is_websocket}}
 {{/operations}}
 {{/apis}}
index 7d138b7..7fe360e 100644 (file)
@@ -137,7 +137,52 @@ fin: __attribute__((unused))
 }
 {{/is_req}}
 {{#is_websocket}}
-static void ast_ari_{{c_name}}_{{c_nickname}}_ws_cb(struct ast_websocket *ws_session,
+static int ast_ari_{{c_name}}_{{c_nickname}}_ws_attempted_cb(struct ast_tcptls_session_instance *ser, struct ast_variable *get_params, struct ast_variable *headers)
+{
+       struct ast_ari_{{c_name}}_{{c_nickname}}_args args = {};
+{{#has_parameters}}
+       int res = 0;
+       RAII_VAR(struct ast_ari_response *, response, NULL, ast_free);
+       struct ast_variable *i;
+{{/has_parameters}}
+
+{{#has_parameters}}
+       response = ast_calloc(1, sizeof(*response));
+       if (!response) {
+               ast_log(LOG_ERROR, "Failed to create response.\n");
+               goto fin;
+       }
+{{/has_parameters}}
+
+{{> param_parsing}}
+
+       res = ast_ari_websocket_{{c_name}}_{{c_nickname}}_attempted(ser, headers, &args);
+
+fin: __attribute__((unused))
+       if (!response) {
+               ast_http_error(ser, 500, "Server Error", "Memory allocation error");
+               res = -1;
+       } else if (response->response_code != 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);
+               }
+               res = -1;
+       }
+{{> param_cleanup}}
+{{#has_parameters}}
+       return res;
+{{/has_parameters}}
+}
+
+static void ast_ari_{{c_name}}_{{c_nickname}}_ws_established_cb(struct ast_websocket *ws_session,
        struct ast_variable *get_params, struct ast_variable *headers)
 {
        struct ast_ari_{{c_name}}_{{c_nickname}}_args args = {};
@@ -175,16 +220,11 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_ws_cb(struct ast_websocket *ws_ses
 
 {{> param_parsing}}
 
-       ast_ari_websocket_{{c_name}}_{{c_nickname}}(session, headers, &args);
+       ast_ari_websocket_{{c_name}}_{{c_nickname}}_established(session, headers, &args);
 
 fin: __attribute__((unused))
        if (response && response->response_code != 0) {
                /* Param parsing failure */
-               /* TODO - ideally, this would return the error code to the
-                * HTTP client; but we've already done the WebSocket
-                * negotiation. Param parsing should happen earlier, but we
-                * need a way to pass it through the WebSocket code to the
-                * callback */
                RAII_VAR(char *, msg, NULL, ast_json_free);
                if (response->message) {
                        msg = ast_json_dump_string(response->message);
@@ -211,16 +251,26 @@ static int load_module(void)
 {
        int res = 0;
 {{#apis}}
+{{#operations}}
 {{#has_websocket}}
+       struct ast_websocket_protocol *protocol;
+
        {{full_name}}.ws_server = ast_websocket_server_create();
        if (!{{full_name}}.ws_server) {
                return AST_MODULE_LOAD_FAILURE;
        }
+
+       protocol = ast_websocket_sub_protocol_alloc("{{websocket_protocol}}");
+       if (!protocol) {
+               ao2_ref({{full_name}}.ws_server, -1);
+               {{full_name}}.ws_server = NULL;
+               return AST_MODULE_LOAD_FAILURE;
+       }
+       protocol->session_attempted = ast_ari_{{c_name}}_{{c_nickname}}_ws_attempted_cb;
+       protocol->session_established = ast_ari_{{c_name}}_{{c_nickname}}_ws_established_cb;
 {{/has_websocket}}
-{{#operations}}
 {{#is_websocket}}
-       res |= ast_websocket_server_add_protocol({{full_name}}.ws_server,
-               "{{websocket_protocol}}", ast_ari_{{c_name}}_{{c_nickname}}_ws_cb);
+       res |= ast_websocket_server_add_protocol2({{full_name}}.ws_server, protocol);
 {{/is_websocket}}
 {{/operations}}
 {{/apis}}