mirror of
				https://github.com/asterisk/asterisk.git
				synced 2025-10-31 02:37:10 +00:00 
			
		
		
		
	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/
		
			
				
	
	
		
			320 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			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);
 | |
| }
 | |
| 
 |