mirror of https://github.com/asterisk/asterisk
This commit adds the ability to make ARI REST requests over the same websocket used to receive events. For full details on how to use the new capability, visit... https://docs.asterisk.org/Configuration/Interfaces/Asterisk-REST-Interface-ARI/ARI-REST-over-WebSocket/ Changes: * Added utilities to http.c: * ast_get_http_method_from_string(). * ast_http_parse_post_form(). * Added utilities to json.c: * ast_json_nvp_array_to_ast_variables(). * ast_variables_to_json_nvp_array(). * Added definitions for new events to carry REST responses. * Created res/ari/ari_websocket_requests.c to house the new request handlers. * Moved non-event specific code out of res/ari/resource_events.c into res/ari/ari_websockets.c * Refactored res/res_ari.c to move non-http code out of ast_ari_callback() (which is http specific) and into ast_ari_invoke() so it can be shared between both the http and websocket transports. UpgradeNote: This commit adds the ability to make ARI REST requests over the same websocket used to receive events. See https://docs.asterisk.org/Configuration/Interfaces/Asterisk-REST-Interface-ARI/ARI-REST-over-WebSocket/22
parent
ecd1a727e8
commit
1442c17141
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2025, Sangoma Technologies Corporation
|
||||
*
|
||||
* George Joseph <gjoseph@sangoma.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
#include "asterisk.h"
|
||||
|
||||
#include "ari_websockets.h"
|
||||
#include "asterisk/ari.h"
|
||||
#include "asterisk/json.h"
|
||||
#include "asterisk/stasis_app.h"
|
||||
|
||||
struct rest_request_msg {
|
||||
char *request_type;
|
||||
char *transaction_id;
|
||||
char *request_id;
|
||||
enum ast_http_method method;
|
||||
char *uri;
|
||||
char *content_type;
|
||||
struct ast_variable *query_strings;
|
||||
struct ast_json *body;
|
||||
};
|
||||
|
||||
static void request_destroy(struct rest_request_msg *request)
|
||||
{
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
ast_free(request->request_type);
|
||||
ast_free(request->transaction_id);
|
||||
ast_free(request->request_id);
|
||||
ast_free(request->uri);
|
||||
ast_free(request->content_type);
|
||||
ast_variables_destroy(request->query_strings);
|
||||
ast_json_unref(request->body);
|
||||
|
||||
ast_free(request);
|
||||
}
|
||||
|
||||
#define SET_RESPONSE_AND_EXIT(_reponse_code, _reponse_text, \
|
||||
_reponse_msg, _remote_addr, _request, _request_msg) \
|
||||
({ \
|
||||
RAII_VAR(char *, _msg_str, NULL, ast_json_free); \
|
||||
if (_request_msg) { \
|
||||
_msg_str = ast_json_dump_string_format(_request_msg, AST_JSON_COMPACT); \
|
||||
if (!_msg_str) { \
|
||||
response->response_code = 500; \
|
||||
response->response_text = "Server error. Out of memory"; \
|
||||
} \
|
||||
} \
|
||||
response->message = ast_json_pack("{ s:s }", \
|
||||
"message", _reponse_msg); \
|
||||
response->response_code = _reponse_code; \
|
||||
response->response_text = _reponse_text; \
|
||||
SCOPE_EXIT_LOG_RTN_VALUE(_request, LOG_WARNING, \
|
||||
"%s: %s Request: %s\n", _remote_addr, _reponse_text, S_OR(_msg_str, "<none>")); \
|
||||
})
|
||||
|
||||
static struct rest_request_msg *parse_rest_request_msg(
|
||||
const char *remote_addr, struct ast_json *request_msg,
|
||||
struct ast_ari_response *response, int debug_app)
|
||||
{
|
||||
struct rest_request_msg *request = NULL;
|
||||
RAII_VAR(char *, body, NULL, ast_free);
|
||||
enum ast_json_nvp_ast_vars_code nvp_code;
|
||||
char *query_string_start = NULL;
|
||||
SCOPE_ENTER(4, "%s: Parsing RESTRequest message\n", remote_addr);
|
||||
|
||||
response->response_code = 200;
|
||||
response->response_text = "OK";
|
||||
|
||||
if (!request_msg) {
|
||||
SET_RESPONSE_AND_EXIT(500,
|
||||
"Server error","No message to parse.",
|
||||
remote_addr, request, NULL);
|
||||
}
|
||||
|
||||
request = ast_calloc(1, sizeof(*request));
|
||||
if (!request) {
|
||||
SET_RESPONSE_AND_EXIT(500,
|
||||
"Server error","Out of memory",
|
||||
remote_addr, request, NULL);
|
||||
}
|
||||
|
||||
/* transaction_id is optional */
|
||||
request->transaction_id = ast_strdup(
|
||||
ast_json_string_get(ast_json_object_get(
|
||||
request_msg, "transaction_id")));
|
||||
|
||||
/* request_id is optional */
|
||||
request->request_id = ast_strdup(
|
||||
ast_json_string_get(ast_json_object_get(
|
||||
request_msg, "request_id")));
|
||||
|
||||
request->request_type = ast_strdup(
|
||||
ast_json_string_get(ast_json_object_get(request_msg, "type")));
|
||||
if (ast_strlen_zero(request->request_type)) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","No 'type' property.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
if (!ast_strings_equal(request->request_type, "RESTRequest")) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unknown request type.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
request->uri = ast_strdup(
|
||||
ast_json_string_get(ast_json_object_get(request_msg, "uri")));
|
||||
if (ast_strlen_zero(request->uri)) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Empty or missing 'uri' property.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
if ((query_string_start = strchr(request->uri, '?')))
|
||||
{
|
||||
*query_string_start = '\0';
|
||||
query_string_start++;
|
||||
request->query_strings = ast_http_parse_post_form(
|
||||
query_string_start, strlen(query_string_start), "application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
request->method = ast_get_http_method_from_string(
|
||||
ast_json_string_get(ast_json_object_get(request_msg, "method")));
|
||||
if (request->method == AST_HTTP_UNKNOWN) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unknown or missing 'method' property.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
/* query_strings is optional */
|
||||
nvp_code = ast_json_nvp_array_to_ast_variables(
|
||||
ast_json_object_get(request_msg, "query_strings"),
|
||||
&request->query_strings);
|
||||
if (nvp_code != AST_JSON_NVP_AST_VARS_CODE_SUCCESS &&
|
||||
nvp_code != AST_JSON_NVP_AST_VARS_CODE_NO_INPUT) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unable to parse 'query_strings' array.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
request->body = ast_json_null();
|
||||
|
||||
body = ast_strdup(ast_json_string_get(
|
||||
ast_json_object_get(request_msg, "message_body")));
|
||||
|
||||
if (ast_strlen_zero(body)) {
|
||||
SCOPE_EXIT_RTN_VALUE(request,
|
||||
"%s: Done parsing RESTRequest message.\n", remote_addr);
|
||||
}
|
||||
|
||||
/* content_type is optional */
|
||||
request->content_type = ast_strdup(
|
||||
ast_json_string_get(ast_json_object_get(request_msg, "content_type")));
|
||||
|
||||
if (ast_strlen_zero(request->content_type)) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","No 'content_type' for 'message_body'.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
if (ast_strings_equal(request->content_type, "application/x-www-form-urlencoded")) {
|
||||
struct ast_variable *vars = ast_http_parse_post_form(body, strlen(body),
|
||||
request->content_type);
|
||||
if (!vars) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unable to parse 'message_body' as 'application/x-www-form-urlencoded'.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
ast_variable_list_append(&request->query_strings, vars);
|
||||
} else if (ast_strings_equal(request->content_type, "application/json")) {
|
||||
struct ast_json_error error;
|
||||
request->body = ast_json_load_buf(body, strlen(body), &error);
|
||||
if (!request->body) {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unable to parse 'message_body' as 'application/json'.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
} else {
|
||||
SET_RESPONSE_AND_EXIT(400,
|
||||
"Bad request","Unknown content type.",
|
||||
remote_addr, request, request_msg);
|
||||
}
|
||||
|
||||
if (TRACE_ATLEAST(3) || debug_app) {
|
||||
struct ast_variable *v = request->query_strings;
|
||||
for (; v; v = v->next) {
|
||||
ast_trace(-1, "Query string: %s=%s\n", v->name, v->value);
|
||||
}
|
||||
}
|
||||
|
||||
SCOPE_EXIT_RTN_VALUE(request,
|
||||
"%s: Done parsing RESTRequest message.\n", remote_addr);
|
||||
}
|
||||
|
||||
static void send_rest_response(
|
||||
struct ari_ws_session *ari_ws_session,
|
||||
const char *remote_addr, const char *app_name,
|
||||
struct rest_request_msg *request,
|
||||
struct ast_ari_response *response, int debug_app)
|
||||
{
|
||||
struct ast_json *app_resp_json = NULL;
|
||||
char *message = NULL;
|
||||
SCOPE_ENTER(4, "%s: Sending REST response %d:%s for uri %s\n",
|
||||
remote_addr, response->response_code, response->response_text,
|
||||
request ? request->uri : "N/A");
|
||||
|
||||
if (response->fd >= 0) {
|
||||
close(response->fd);
|
||||
response->response_code = 406;
|
||||
response->response_text = "Not Acceptable. Use HTTP GET";
|
||||
} else if (response->message && !ast_json_is_null(response->message)) {
|
||||
message = ast_json_dump_string_format(response->message, AST_JSON_COMPACT);
|
||||
ast_json_unref(response->message);
|
||||
}
|
||||
|
||||
app_resp_json = ast_json_pack(
|
||||
"{s:s, s:s*, s:s*, s:i, s:s, s:s, s:s*, s:s* }",
|
||||
"type", "RESTResponse",
|
||||
"transaction_id", request ? S_OR(request->transaction_id, "") : "",
|
||||
"request_id", request ? S_OR(request->request_id, "") : "",
|
||||
"status_code", response->response_code,
|
||||
"reason_phrase", response->response_text,
|
||||
"uri", request ? S_OR(request->uri, "") : "",
|
||||
"content_type", message ? "application/json" : NULL,
|
||||
"message_body", message);
|
||||
|
||||
ast_json_free(message);
|
||||
if (!app_resp_json || ast_json_is_null(app_resp_json)) {
|
||||
SCOPE_EXIT_LOG_RTN(LOG_WARNING,
|
||||
"%s: Failed to pack JSON response for request %s\n",
|
||||
remote_addr, request ? request->uri : "N/A");
|
||||
}
|
||||
|
||||
SCOPE_CALL(-1, ari_websocket_send_event, ari_ws_session,
|
||||
app_name, app_resp_json, debug_app);
|
||||
|
||||
ast_json_unref(app_resp_json);
|
||||
|
||||
SCOPE_EXIT("%s: Done. response: %d : %s\n",
|
||||
remote_addr,
|
||||
response->response_code,
|
||||
response->response_text);
|
||||
}
|
||||
|
||||
int ari_websocket_process_request(struct ari_ws_session *ari_ws_session,
|
||||
const char *remote_addr, struct ast_variable *upgrade_headers,
|
||||
const char *app_name, struct ast_json *request_msg)
|
||||
{
|
||||
int debug_app = stasis_app_get_debug_by_name(app_name);
|
||||
RAII_VAR(struct rest_request_msg *, request, NULL, request_destroy);
|
||||
struct ast_ari_response response = { .fd = -1, 0 };
|
||||
|
||||
SCOPE_ENTER(3, "%s: New WebSocket Msg\n", remote_addr);
|
||||
|
||||
if (TRACE_ATLEAST(3) || debug_app) {
|
||||
char *str = ast_json_dump_string_format(request_msg, AST_JSON_PRETTY);
|
||||
/* If we can't allocate a string, we can't respond to the client either. */
|
||||
if (!str) {
|
||||
SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: Failed to dump JSON request\n",
|
||||
remote_addr);
|
||||
}
|
||||
ast_verbose("<--- Received ARI message from %s --->\n%s\n",
|
||||
remote_addr, str);
|
||||
ast_json_free(str);
|
||||
}
|
||||
|
||||
request = SCOPE_CALL_WITH_RESULT(-1, struct rest_request_msg *,
|
||||
parse_rest_request_msg, remote_addr, request_msg, &response, debug_app);
|
||||
|
||||
if (!request || response.response_code != 200) {
|
||||
SCOPE_CALL(-1, send_rest_response, ari_ws_session,
|
||||
remote_addr, app_name, request, &response, debug_app);
|
||||
SCOPE_EXIT_RTN_VALUE(0, "%s: Done with message\n", remote_addr);
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't actually use the headers in the response
|
||||
* but we have to allocate it because ast_ari_invoke
|
||||
* and the resource handlers expect it.
|
||||
*/
|
||||
response.headers = ast_str_create(80);
|
||||
if (!response.headers) {
|
||||
/* If we can't allocate a string, we can't respond to the client either. */
|
||||
SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: Failed allocate headers string\n",
|
||||
remote_addr);
|
||||
}
|
||||
|
||||
SCOPE_CALL(-1, ast_ari_invoke, NULL, ARI_INVOKE_SOURCE_WEBSOCKET,
|
||||
NULL, request->uri, request->method, request->query_strings,
|
||||
upgrade_headers, request->body, &response);
|
||||
|
||||
ast_free(response.headers);
|
||||
|
||||
if (response.no_response) {
|
||||
SCOPE_EXIT_RTN_VALUE(0, "No response needed\n");
|
||||
}
|
||||
|
||||
SCOPE_CALL(-1, send_rest_response, ari_ws_session,
|
||||
remote_addr, app_name, request, &response, debug_app);
|
||||
|
||||
SCOPE_EXIT_RTN_VALUE(0, "%s: Done with message\n", remote_addr);
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Asterisk -- An open source telephony toolkit.
|
||||
*
|
||||
* Copyright (C) 2013, Digium, Inc.
|
||||
*
|
||||
* David M. Lee, II <dlee@digium.com>
|
||||
*
|
||||
* See http://www.asterisk.org for more information about
|
||||
* the Asterisk project. Please do not directly contact
|
||||
* any of the maintainers of this project for assistance;
|
||||
* the project provides a web site, mailing lists and IRC
|
||||
* channels for your use.
|
||||
*
|
||||
* This program is free software, distributed under the terms of
|
||||
* the GNU General Public License Version 2. See the LICENSE file
|
||||
* at the top of the source tree.
|
||||
*/
|
||||
|
||||
#ifndef ARI_WEBSOCKETS_H_
|
||||
#define ARI_WEBSOCKETS_H_
|
||||
|
||||
/*! \file
|
||||
*
|
||||
* \brief Internal API's for websockets.
|
||||
* \author David M. Lee, II <dlee@digium.com>
|
||||
*/
|
||||
|
||||
#include "asterisk/http.h"
|
||||
#include "asterisk/json.h"
|
||||
#include "asterisk/vector.h"
|
||||
|
||||
struct ast_ari_events_event_websocket_args;
|
||||
|
||||
/* Forward-declare websocket structs. This avoids including http_websocket.h,
|
||||
* which causes optional_api stuff to happen, which makes optional_api more
|
||||
* difficult to debug. */
|
||||
|
||||
//struct ast_websocket_server;
|
||||
struct ast_websocket;
|
||||
|
||||
struct ari_ws_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
|
||||
the websocket session. */
|
||||
AST_VECTOR(, struct ast_json *) message_queue; /*!< Container for holding delayed messages. */
|
||||
char *app_name; /*!< The name of the Stasis application. */
|
||||
char session_id[]; /*!< The id for the websocket session. */
|
||||
};
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Send a JSON event to a websocket.
|
||||
*
|
||||
* \param ari_ws_session ARI websocket session
|
||||
* \param app_name Application name
|
||||
* \param message JSON message
|
||||
* \param debug_app Debug flag for application
|
||||
*/
|
||||
void ari_websocket_send_event(struct ari_ws_session *ari_ws_session,
|
||||
const char *app_name, struct ast_json *message, int debug_app);
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Process an ARI REST over Websocket request
|
||||
*
|
||||
* \param ari_ws_session ARI websocket session
|
||||
* \param remote_addr Remote address for log messages
|
||||
* \param upgrade_headers HTTP headers from the upgrade request
|
||||
* \param app_name Application name
|
||||
* \param msg JSON Request message
|
||||
* \retval 0 on success, -1 on failure
|
||||
*/
|
||||
int ari_websocket_process_request(struct ari_ws_session *ast_ws_session,
|
||||
const char *remote_addr, struct ast_variable *upgrade_headers,
|
||||
const char *app_name, struct ast_json *msg);
|
||||
|
||||
/*!
|
||||
* \brief Wrapper for invoking the websocket code for an incoming connection.
|
||||
*
|
||||
* \param ws_server WebSocket server to invoke.
|
||||
* \param ser HTTP session.
|
||||
* \param uri Requested URI.
|
||||
* \param method Requested HTTP method.
|
||||
* \param get_params Parsed query parameters.
|
||||
* \param headers Parsed HTTP headers.
|
||||
*/
|
||||
void ari_handle_websocket(struct ast_tcptls_session_instance *ser,
|
||||
const char *uri, enum ast_http_method method,
|
||||
struct ast_variable *get_params,
|
||||
struct ast_variable *headers);
|
||||
|
||||
int ari_websocket_unload_module(void);
|
||||
int ari_websocket_load_module(void);
|
||||
|
||||
#endif /* ARI_WEBSOCKETS_H_ */
|
Loading…
Reference in new issue