mirror of
				https://github.com/asterisk/asterisk.git
				synced 2025-10-31 02:37:10 +00:00 
			
		
		
		
	ARI: Add the ability to download the media associated with a stored recording
This patch adds a new feature to ARI that allows a client to download
the media associated with a stored recording. The new route is
/recordings/stored/{name}/file, and transmits the underlying binary file
using Asterisk's HTTP server's underlying file transfer facilities.
Because this REST route returns non-JSON, a few small enhancements had
to be made to the Python Swagger generation code, as well as the
mustache templates that generate the ARI bindings.
ASTERISK-26042 #close
Change-Id: I49ec5c4afdec30bb665d9c977ab423b5387e0181
			
			
This commit is contained in:
		
							
								
								
									
										4
									
								
								CHANGES
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								CHANGES
									
									
									
									
									
								
							| @@ -32,6 +32,10 @@ ARI | ||||
|    back to the resource. The "PlaybackFinished" event is raised when all media | ||||
|    URIs are done. | ||||
|  | ||||
|  * Stored recordings now allow for the media associated with a stored recording | ||||
|    to be retrieved. The new route, GET /recordings/stored/{name}/file, will | ||||
|    transmit the raw media file to the requester as binary. | ||||
|  | ||||
|  | ||||
| Applications | ||||
| ------------------ | ||||
|   | ||||
| @@ -95,6 +95,8 @@ struct ast_ari_response { | ||||
| 	/*! HTTP response code. | ||||
| 	 * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */ | ||||
| 	int response_code; | ||||
| 	/*! File descriptor for whatever file we want to respond with */ | ||||
| 	int fd; | ||||
| 	/*! Corresponding text for the response code */ | ||||
| 	const char *response_text; /* Shouldn't http.c handle this? */ | ||||
| 	/*! Flag to indicate that no further response is needed */ | ||||
|   | ||||
| @@ -48,6 +48,30 @@ struct stasis_app_stored_recording; | ||||
| const char *stasis_app_stored_recording_get_file( | ||||
| 	struct stasis_app_stored_recording *recording); | ||||
|  | ||||
| /*! | ||||
|  * \brief Returns the full filename, with extension, for this recording. | ||||
|  * \since 14.0.0 | ||||
|  * | ||||
|  * \param recording Recording to query. | ||||
|  * | ||||
|  * \return Absolute path to the recording file, with the extension. | ||||
|  * \return \c NULL on error | ||||
|  */ | ||||
| const char *stasis_app_stored_recording_get_filename( | ||||
| 	struct stasis_app_stored_recording *recording); | ||||
|  | ||||
| /*! | ||||
|  * \brief Returns the extension for this recording. | ||||
|  * \since 14.0.0 | ||||
|  * | ||||
|  * \param recording Recording to query. | ||||
|  * | ||||
|  * \return The extension associated with this recording. | ||||
|  * \return \c NULL on error | ||||
|  */ | ||||
| const char *stasis_app_stored_recording_get_extension( | ||||
| 	struct stasis_app_stored_recording *recording); | ||||
|  | ||||
| /*! | ||||
|  * \brief Convert stored recording info to JSON. | ||||
|  * | ||||
|   | ||||
| @@ -101,6 +101,50 @@ void ast_ari_recordings_get_stored(struct ast_variable *headers, | ||||
| 	ast_ari_response_ok(response, json); | ||||
| } | ||||
|  | ||||
| void ast_ari_recordings_get_stored_file(struct ast_tcptls_session_instance *ser, | ||||
| 	struct ast_variable *headers, struct ast_ari_recordings_get_stored_file_args *args, | ||||
| 	struct ast_ari_response *response) | ||||
| { | ||||
| 	RAII_VAR(struct stasis_app_stored_recording *, recording, | ||||
| 		stasis_app_stored_recording_find_by_name(args->recording_name), | ||||
| 		ao2_cleanup); | ||||
| 	static const char *format_type_names[AST_MEDIA_TYPE_TEXT + 1] = { | ||||
| 		[AST_MEDIA_TYPE_UNKNOWN] = "binary", | ||||
| 		[AST_MEDIA_TYPE_AUDIO] = "audio", | ||||
| 		[AST_MEDIA_TYPE_VIDEO] = "video", | ||||
| 		[AST_MEDIA_TYPE_IMAGE] = "image", | ||||
| 		[AST_MEDIA_TYPE_TEXT] = "text", | ||||
| 	}; | ||||
| 	struct ast_format *format; | ||||
|  | ||||
| 	response->message = ast_json_null(); | ||||
|  | ||||
| 	if (!recording) { | ||||
| 		ast_ari_response_error(response, 404, "Not Found", | ||||
| 			"Recording not found"); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	format = ast_get_format_for_file_ext(stasis_app_stored_recording_get_extension(recording)); | ||||
| 	if (!format) { | ||||
| 		ast_ari_response_error(response, 500, "Internal Server Error", | ||||
| 			"Format specified by recording not available or loaded"); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	response->fd = open(stasis_app_stored_recording_get_filename(recording), O_RDONLY); | ||||
| 	if (response->fd < 0) { | ||||
| 		ast_ari_response_error(response, 403, "Forbidden", | ||||
| 			"Recording could not be opened"); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	ast_str_append(&response->headers, 0, "Content-Type: %s/%s\r\n", | ||||
| 		format_type_names[ast_format_get_type(format)], | ||||
| 		stasis_app_stored_recording_get_extension(recording)); | ||||
| 	ast_ari_response_ok(response, ast_json_null()); | ||||
| } | ||||
|  | ||||
| void ast_ari_recordings_copy_stored(struct ast_variable *headers, | ||||
| 	struct ast_ari_recordings_copy_stored_args *args, | ||||
| 	struct ast_ari_response *response) | ||||
|   | ||||
| @@ -76,6 +76,20 @@ struct ast_ari_recordings_delete_stored_args { | ||||
|  * \param[out] response HTTP response | ||||
|  */ | ||||
| void ast_ari_recordings_delete_stored(struct ast_variable *headers, struct ast_ari_recordings_delete_stored_args *args, struct ast_ari_response *response); | ||||
| /*! Argument struct for ast_ari_recordings_get_stored_file() */ | ||||
| struct ast_ari_recordings_get_stored_file_args { | ||||
| 	/*! The name of the recording */ | ||||
| 	const char *recording_name; | ||||
| }; | ||||
| /*! | ||||
|  * \brief Get the file associated with the stored recording. | ||||
|  * | ||||
|  * \param ser TCP/TLS session instance | ||||
|  * \param headers HTTP headers | ||||
|  * \param args Swagger parameters | ||||
|  * \param[out] response HTTP response | ||||
|  */ | ||||
| void ast_ari_recordings_get_stored_file(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_recordings_get_stored_file_args *args, struct ast_ari_response *response); | ||||
| /*! Argument struct for ast_ari_recordings_copy_stored() */ | ||||
| struct ast_ari_recordings_copy_stored_args { | ||||
| 	/*! The name of the recording to copy */ | ||||
|   | ||||
| @@ -870,7 +870,7 @@ static int ast_ari_callback(struct ast_tcptls_session_instance *ser, | ||||
| 	RAII_VAR(struct ast_ari_conf *, conf, NULL, ao2_cleanup); | ||||
| 	RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free); | ||||
| 	RAII_VAR(struct ast_ari_conf_user *, user, NULL, ao2_cleanup); | ||||
| 	struct ast_ari_response response = {}; | ||||
| 	struct ast_ari_response response = { .fd = -1, 0 }; | ||||
| 	RAII_VAR(struct ast_variable *, post_vars, NULL, ast_variables_destroy); | ||||
|  | ||||
| 	if (!response_body) { | ||||
| @@ -1011,11 +1011,14 @@ request_failed: | ||||
| 		response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body)); | ||||
| 	ast_http_send(ser, method, response.response_code, | ||||
| 		      response.response_text, response.headers, response_body, | ||||
| 		      0, 0); | ||||
| 		      response.fd != -1 ? response.fd : 0, 0); | ||||
| 	/* ast_http_send takes ownership, so we don't have to free them */ | ||||
| 	response_body = NULL; | ||||
|  | ||||
| 	ast_json_unref(response.message); | ||||
| 	if (response.fd >= 0) { | ||||
| 		close(response.fd); | ||||
| 	} | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| @@ -1023,7 +1026,6 @@ static struct ast_http_uri http_uri = { | ||||
| 	.callback = ast_ari_callback, | ||||
| 	.description = "Asterisk RESTful API", | ||||
| 	.uri = "ari", | ||||
|  | ||||
| 	.has_subtree = 1, | ||||
| 	.data = NULL, | ||||
| 	.key = __FILE__, | ||||
|   | ||||
| @@ -218,6 +218,65 @@ static void ast_ari_recordings_delete_stored_cb( | ||||
| 	} | ||||
| #endif /* AST_DEVMODE */ | ||||
|  | ||||
| fin: __attribute__((unused)) | ||||
| 	return; | ||||
| } | ||||
| /*! | ||||
|  * \brief Parameter parsing callback for /recordings/stored/{recordingName}/file. | ||||
|  * \param get_params GET parameters in the HTTP request. | ||||
|  * \param path_vars Path variables extracted from the request. | ||||
|  * \param headers HTTP headers. | ||||
|  * \param[out] response Response to the HTTP request. | ||||
|  */ | ||||
| static void ast_ari_recordings_get_stored_file_cb( | ||||
| 	struct ast_tcptls_session_instance *ser, | ||||
| 	struct ast_variable *get_params, struct ast_variable *path_vars, | ||||
| 	struct ast_variable *headers, struct ast_ari_response *response) | ||||
| { | ||||
| 	struct ast_ari_recordings_get_stored_file_args args = {}; | ||||
| 	struct ast_variable *i; | ||||
| 	RAII_VAR(struct ast_json *, body, NULL, ast_json_unref); | ||||
| #if defined(AST_DEVMODE) | ||||
| 	int is_valid; | ||||
| 	int code; | ||||
| #endif /* AST_DEVMODE */ | ||||
|  | ||||
| 	for (i = path_vars; i; i = i->next) { | ||||
| 		if (strcmp(i->name, "recordingName") == 0) { | ||||
| 			args.recording_name = (i->value); | ||||
| 		} else | ||||
| 		{} | ||||
| 	} | ||||
| 	ast_ari_recordings_get_stored_file(ser, headers, &args, response); | ||||
| #if defined(AST_DEVMODE) | ||||
| 	code = response->response_code; | ||||
|  | ||||
| 	switch (code) { | ||||
| 	case 0: /* Implementation is still a stub, or the code wasn't set */ | ||||
| 		is_valid = response->message == NULL; | ||||
| 		break; | ||||
| 	case 500: /* Internal Server Error */ | ||||
| 	case 501: /* Not Implemented */ | ||||
| 	case 404: /* Recording not found */ | ||||
| 		is_valid = 1; | ||||
| 		break; | ||||
| 	default: | ||||
| 		if (200 <= code && code <= 299) { | ||||
| 			/* No validation on a raw binary response */ | ||||
| 			is_valid = 1; | ||||
| 		} else { | ||||
| 			ast_log(LOG_ERROR, "Invalid error response %d for /recordings/stored/{recordingName}/file\n", code); | ||||
| 			is_valid = 0; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (!is_valid) { | ||||
| 		ast_log(LOG_ERROR, "Response validation failed for /recordings/stored/{recordingName}/file\n"); | ||||
| 		ast_ari_response_error(response, 500, | ||||
| 			"Internal Server Error", "Response validation failed"); | ||||
| 	} | ||||
| #endif /* AST_DEVMODE */ | ||||
|  | ||||
| fin: __attribute__((unused)) | ||||
| 	return; | ||||
| } | ||||
| @@ -737,6 +796,15 @@ fin: __attribute__((unused)) | ||||
| 	return; | ||||
| } | ||||
|  | ||||
| /*! \brief REST handler for /api-docs/recordings.{format} */ | ||||
| static struct stasis_rest_handlers recordings_stored_recordingName_file = { | ||||
| 	.path_segment = "file", | ||||
| 	.callbacks = { | ||||
| 		[AST_HTTP_GET] = ast_ari_recordings_get_stored_file_cb, | ||||
| 	}, | ||||
| 	.num_children = 0, | ||||
| 	.children = {  } | ||||
| }; | ||||
| /*! \brief REST handler for /api-docs/recordings.{format} */ | ||||
| static struct stasis_rest_handlers recordings_stored_recordingName_copy = { | ||||
| 	.path_segment = "copy", | ||||
| @@ -754,8 +822,8 @@ static struct stasis_rest_handlers recordings_stored_recordingName = { | ||||
| 		[AST_HTTP_GET] = ast_ari_recordings_get_stored_cb, | ||||
| 		[AST_HTTP_DELETE] = ast_ari_recordings_delete_stored_cb, | ||||
| 	}, | ||||
| 	.num_children = 1, | ||||
| 	.children = { &recordings_stored_recordingName_copy, } | ||||
| 	.num_children = 2, | ||||
| 	.children = { &recordings_stored_recordingName_file,&recordings_stored_recordingName_copy, } | ||||
| }; | ||||
| /*! \brief REST handler for /api-docs/recordings.{format} */ | ||||
| static struct stasis_rest_handlers recordings_stored = { | ||||
|   | ||||
| @@ -62,6 +62,24 @@ const char *stasis_app_stored_recording_get_file( | ||||
| 	return recording->file; | ||||
| } | ||||
|  | ||||
| const char *stasis_app_stored_recording_get_filename( | ||||
| 	struct stasis_app_stored_recording *recording) | ||||
| { | ||||
| 	if (!recording) { | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	return recording->file_with_ext; | ||||
| } | ||||
|  | ||||
| const char *stasis_app_stored_recording_get_extension( | ||||
| 	struct stasis_app_stored_recording *recording) | ||||
| { | ||||
| 	if (!recording) { | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	return recording->format; | ||||
| } | ||||
|  | ||||
| /*! | ||||
|  * \brief Split a path into directory and file, resolving canonical directory. | ||||
|  * | ||||
|   | ||||
| @@ -82,11 +82,19 @@ int ast_ari_{{c_name}}_{{c_nickname}}_parse_body( | ||||
|  * {{{notes}}} | ||||
| {{/notes}} | ||||
|  * | ||||
| {{#is_binary_response}} | ||||
|  * \param ser TCP/TLS session instance | ||||
| {{/is_binary_response}} | ||||
|  * \param headers HTTP headers | ||||
|  * \param args Swagger parameters | ||||
|  * \param[out] response HTTP response | ||||
|  */ | ||||
| {{^is_binary_response}} | ||||
| 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_binary_response}} | ||||
| {{#is_binary_response}} | ||||
| void ast_ari_{{c_name}}_{{c_nickname}}(struct ast_tcptls_session_instance *ser, struct ast_variable *headers, struct ast_ari_{{c_name}}_{{c_nickname}}_args *args, struct ast_ari_response *response); | ||||
| {{/is_binary_response}} | ||||
| {{/is_req}} | ||||
| {{#is_websocket}} | ||||
|  | ||||
|   | ||||
| @@ -91,7 +91,12 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb( | ||||
| #endif /* AST_DEVMODE */ | ||||
|  | ||||
| {{> param_parsing}} | ||||
| {{^is_binary_response}} | ||||
| 	ast_ari_{{c_name}}_{{c_nickname}}(headers, &args, response); | ||||
| {{/is_binary_response}} | ||||
| {{#is_binary_response}} | ||||
| 	ast_ari_{{c_name}}_{{c_nickname}}(ser, headers, &args, response); | ||||
| {{/is_binary_response}} | ||||
| #if defined(AST_DEVMODE) | ||||
| 	code = response->response_code; | ||||
|  | ||||
| @@ -114,8 +119,14 @@ static void ast_ari_{{c_name}}_{{c_nickname}}_cb( | ||||
| 				ast_ari_validate_{{c_singular_name}}_fn()); | ||||
| {{/is_list}} | ||||
| {{^is_list}} | ||||
| {{^is_binary_response}} | ||||
| 			is_valid = ast_ari_validate_{{c_name}}( | ||||
| 				response->message); | ||||
| {{/is_binary_response}} | ||||
| {{#is_binary_response}} | ||||
| 			/* No validation on a raw binary response */ | ||||
| 			is_valid = 1; | ||||
| {{/is_binary_response}} | ||||
| {{/is_list}} | ||||
| {{/response_class}} | ||||
| 		} else { | ||||
|   | ||||
| @@ -332,6 +332,7 @@ class SwaggerType(Stringify): | ||||
|         self.is_list = None | ||||
|         self.singular_name = None | ||||
|         self.is_primitive = None | ||||
|         self.is_binary = None | ||||
|  | ||||
|     def load(self, type_name, processor, context): | ||||
|         # Some common errors | ||||
| @@ -346,6 +347,7 @@ class SwaggerType(Stringify): | ||||
|         else: | ||||
|             self.singular_name = self.name | ||||
|         self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES | ||||
|         self.is_binary = (self.singular_name == 'binary') | ||||
|         processor.process_type(self, context) | ||||
|         return self | ||||
|  | ||||
| @@ -401,6 +403,7 @@ class Operation(Stringify): | ||||
|         self.has_header_parameters = self.header_parameters and True | ||||
|         self.has_parameters = self.has_query_parameters or \ | ||||
|             self.has_path_parameters or self.has_header_parameters | ||||
|         self.is_binary_response = self.response_class.is_binary | ||||
|  | ||||
|         # Body param is different, since there's at most one | ||||
|         self.body_parameter = [ | ||||
|   | ||||
| @@ -69,6 +69,38 @@ | ||||
| 				} | ||||
| 			] | ||||
| 		}, | ||||
| 		{ | ||||
| 			"path": "/recordings/stored/{recordingName}/file", | ||||
| 			"description": "The actual file associated with the stored recording", | ||||
| 			"operations": [ | ||||
| 				{ | ||||
| 					"httpMethod": "GET", | ||||
| 					"summary": "Get the file associated with the stored recording.", | ||||
| 					"nickname": "getStoredFile", | ||||
| 					"responseClass": "binary", | ||||
| 					"parameters": [ | ||||
| 						{ | ||||
| 							"name": "recordingName", | ||||
| 							"description": "The name of the recording", | ||||
| 							"paramType": "path", | ||||
| 							"required": true, | ||||
| 							"allowMultiple": false, | ||||
| 							"dataType": "string" | ||||
| 						} | ||||
| 					], | ||||
| 					"errorResponses": [ | ||||
| 						{ | ||||
| 							"code": 403, | ||||
| 							"reason": "The recording file could not be opened" | ||||
| 						}, | ||||
| 						{ | ||||
| 							"code": 404, | ||||
| 							"reason": "Recording not found" | ||||
| 						} | ||||
| 					] | ||||
| 				} | ||||
| 			] | ||||
| 		}, | ||||
| 		{ | ||||
| 			"path": "/recordings/stored/{recordingName}/copy", | ||||
| 			"description": "Copy an individual recording", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user