Files
asterisk/res/ari/ari_websocket_requests.c
George Joseph 64aeb20724 ARI: REST over Websocket
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/

(cherry picked from commit 6bc055416b)
2025-05-01 12:41:16 +00:00

320 lines
10 KiB
C

/*
* 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);
}