diff --git a/configs/samples/aeap.conf.sample b/configs/samples/aeap.conf.sample index 8941dd1f0375b1f24e87a02a97f7b808bea70aa5..2431ea3259e190dd0f005cac2fa91917aa489965 100644 --- a/configs/samples/aeap.conf.sample +++ b/configs/samples/aeap.conf.sample @@ -2,14 +2,20 @@ ; This file is used by the res_aeap module to configure parameters ; used for AEAP applications. ; -;[myserver] +;[myclient] ; -; type must be "server". -;type=server +; type must be "client". +;type=client ; -; server_url must be a websocket URL (ws or wss). -;server_url +; URL used to connect to a server. It must be a websocket URL (ws or wss). +;url=ws://127.0.0.1:9099 ; -; codecs is an optional list of codecs that will be used over the codecs -; specified on an endpoint if this option is present. -;codecs=ulaw,alaw,g722,opus +; codecs is comma separated string of allowed/disallowed codec names. +;codecs=!all,ulaw,alaw,opus +; +; protocol is the implementation specific sub-protocol +;protocol=speech_to_text +; +; "@" parameters can be specified and are used to to set custom values to +; be passed as "params" in the initial "setup" request. +;@language=en-US diff --git a/include/asterisk/res_aeap.h b/include/asterisk/res_aeap.h new file mode 100644 index 0000000000000000000000000000000000000000..0a1747e923f6af084a2ae3923fa534db797aec4c --- /dev/null +++ b/include/asterisk/res_aeap.h @@ -0,0 +1,370 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*! \file + * \brief Asterisk External Application Protocol API + */ + +#ifndef AST_RES_AEAP_H +#define AST_RES_AEAP_H + +#include <stdint.h> + +struct ao2_container; +struct ast_sorcery; +struct ast_variable; + +struct ast_aeap_client_config; +struct ast_aeap_message; + +#define AEAP_CONFIG_CLIENT "client" + +/*! + * \brief Retrieve the AEAP sorcery object + * + * \returns the AEAP sorcery object + */ +struct ast_sorcery *ast_aeap_sorcery(void); + +/*! + * \brief Retrieve a listing of all client configuration objects by protocol. + * + * \note Caller is responsible for the returned container's reference. + * + * \param protocol An optional protocol to filter on (if NULL returns all client configs) + * + * \returns A container of client configuration objects + */ +struct ao2_container *ast_aeap_client_configs_get(const char *protocol); + +/*! + * \brief Retrieve codec capabilities from the configuration + * + * \param config A configuration object + * + * \returns The configuration's codec capabilities + */ +const struct ast_format_cap *ast_aeap_client_config_codecs(const struct ast_aeap_client_config *cfg); + +/*! + * \brief Check a given protocol against that in an Asterisk external application configuration + * + * \param config A configuration object + * \param protocol The protocol to check + * + * \returns True if the configuration's protocol matches, false otherwise + */ +int ast_aeap_client_config_has_protocol(const struct ast_aeap_client_config *cfg, + const char *protocol); + +/*! + * \brief Retrieve a list of custom configuration fields + * + * \param id configuration id/sorcery lookup key + * + * \returns variables, or NULL on error + */ +struct ast_variable *ast_aeap_custom_fields_get(const char *id); + +/*! + * \brief An Asterisk external application object + * + * Connects to an external application, sending and receiving data, and + * dispatches received data to registered handlers. + */ +struct ast_aeap; + +/*! + * \brief Event raised when a message is received + * + * \param aeap An Asterisk external application object + * \param message The received message + * \param obj Associated user object + * + * \returns 0 on if message handled, otherwise non-zero + */ +typedef int (*ast_aeap_on_message)(struct ast_aeap *aeap, struct ast_aeap_message *message, void *obj); + +/*! + * \brief An Asterisk external application message handler + * + * Used to register message handlers with an AEAP object. + */ +struct ast_aeap_message_handler { + /*! The handler name */ + const char *name; + /*! Callback triggered when on a name match */ + ast_aeap_on_message on_message; +}; + +/*! + * \brief Event raised when a sent message does not receive a reply within + * a specified time interval + * + * \param aeap An Asterisk external application object + * \param message The message sent that received no response + * \param obj Associated user object + */ +typedef void (*ast_aeap_on_timeout)(struct ast_aeap *aeap, struct ast_aeap_message *message, void *obj); + +/*! + * \brief Callback to cleanup a user object + * + * \param obj The user object + */ +typedef void (*ast_aeap_user_obj_cleanup)(void *obj); + +/*! + * \brief Supported Asterisk external application data types + */ +enum AST_AEAP_DATA_TYPE { + AST_AEAP_DATA_TYPE_NONE, + AST_AEAP_DATA_TYPE_BINARY, + AST_AEAP_DATA_TYPE_STRING, +}; + +/*! + * \brief Callbacks and other parameters used by an Asterisk external application object + */ +struct ast_aeap_params { + /*! + * If true pass along error messages to the implementation. + * Otherwise log it only, and consider it handled. + */ + unsigned int emit_error; + + /*! The message type used for communication */ + const struct ast_aeap_message_type *msg_type; + + /*! Response handlers array */ + const struct ast_aeap_message_handler *response_handlers; + /*! The number of response handlers */ + uintmax_t response_handlers_size; + + /*! Request handlers array */ + const struct ast_aeap_message_handler *request_handlers; + /*! The number of request handlers */ + uintmax_t request_handlers_size; + + /*! + * \brief Raised when binary data is received + * + * \param aeap An Asterisk external application object + * \param buf The buffer containing binary data + * \param size The size of the buffer + */ + void (*on_binary)(struct ast_aeap *aeap, const void *buf, intmax_t size); + + /*! + * \brief Raised when string data is received + * + * \param aeap An Asterisk external application object + * \param buf The buffer containing string data + * \param size The size/length of the string + */ + void (*on_string)(struct ast_aeap *aeap, const char *buf, intmax_t size); + + /*! + * \brief Raised when an error occurs during reading + * + * \note This is an AEAP transport level read error event + * + * \note When this event is triggered the client has also + * been disconnected. + * + * \param aeap An Asterisk external application object + */ + void (*on_error)(struct ast_aeap *aeap); +}; + +/*! + * \brief Create an Asterisk external application object + * + * \param type The type of underlying transport + * \param params Callbacks and other parameters to use + * + * \returns A new ao2 reference counted aeap object, or NULL on error + */ +struct ast_aeap *ast_aeap_create(const char *type, const struct ast_aeap_params *params); + +/*! + * \brief Create an Asterisk external application object by sorcery id + * + * \param id The sorcery id to lookup + * \param params Callbacks and other parameters to use + * + * \returns A new ao2 reference counted aeap object, or NULL on error + */ +struct ast_aeap *ast_aeap_create_by_id(const char *id, const struct ast_aeap_params *params); + +/*! + * \brief Connect to an external application + * + * \param aeap An Asterisk external application object + * \param url The url to connect to + * \param protocol A protocol to use + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns 0 if able to connect, -1 on error + */ +int ast_aeap_connect(struct ast_aeap *aeap, const char *url, const char *protocol, int timeout); + +/*! + * \brief Create and connect to an Asterisk external application by sorcery id + * + * \param id The sorcery id to lookup + * \param params Callbacks and other parameters to use + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns A new ao2 reference counted aeap object, or NULL on error + */ +struct ast_aeap *ast_aeap_create_and_connect_by_id(const char *id, + const struct ast_aeap_params *params, int timeout); + +/*! + * \brief Create and connect to an Asterisk external application + * + * \param type The type of client connection to make + * \param params Callbacks and other parameters to use + * \param url The url to connect to + * \param protocol A protocol to use + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns A new ao2 reference counted aeap object, or NULL on error + */ +struct ast_aeap *ast_aeap_create_and_connect(const char *type, + const struct ast_aeap_params *params, const char *url, const char *protocol, int timeout); + +/*! + * \brief Disconnect an Asterisk external application object + * + * \note Depending on the underlying transport this call may block + * + * \param aeap An Asterisk external application object + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_disconnect(struct ast_aeap *aeap); + +/*! + * \brief Register a user data object + * + * \note The "cleanup" is called on un-register, if one is specified + * + * \param aeap An Asterisk external application object + * \param id The look up id for the object + * \param obj The user object to register + * \param cleanup Optional user object clean up callback + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_user_data_register(struct ast_aeap *aeap, const char *id, void *obj, + ast_aeap_user_obj_cleanup cleanup); + +/*! + * \brief Un-register a user data object + * + * \note If specified on register, the "cleanup" callback is called during unregister. + * + * \param aeap An Asterisk external application object + * \param id The look up id for the object + */ +void ast_aeap_user_data_unregister(struct ast_aeap *aeap, const char *id); + +/*! + * \brief Retrieve a registered user data object by its id + * + * \note Depending on how it was registered the returned user data object's lifetime + * may be managed by the given "aeap" object. If it was registered with a cleanup + * handler that [potentially] frees it the caller of this function must ensure + * it's done using the returned object before it's unregistered. + * + * \param data A user data object + * + * \returns A user data object + */ +void *ast_aeap_user_data_object_by_id(struct ast_aeap *aeap, const char *id); + +/*! + * \brief Send a binary data to an external application + * + * \param aeap An Asterisk external application object + * \param buf Binary data to send + * \param size The size of the binary data + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_send_binary(struct ast_aeap *aeap, const void *buf, uintmax_t size); + +/*! + * \brief Send a message to an external application + * + * \note "Steals" the given message reference, thus callers are not required to un-ref + * the message object after calling this function. + * + * \param aeap An Asterisk external application object + * \param msg The message to send + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_send_msg(struct ast_aeap *aeap, struct ast_aeap_message *msg); + +/*! + * \brief Parameters to be used when sending a transaction based message + */ +struct ast_aeap_tsx_params { + /*! The message to send */ + struct ast_aeap_message *msg; + /*! The amount of time (in milliseconds) to wait for a received message */ + int timeout; + /*! Optional callback raised when no message is received in an allotted time */ + ast_aeap_on_timeout on_timeout; + /*! Whether or not to block the current thread, and wait for a received message */ + int wait; + /*! + * Optional user object to pass to handlers. User is responsible for object's lifetime + * unless an obj_cleanup callback is specified that handles its cleanup (e.g. freeing + * of memory). + */ + void *obj; + /*! + * Optional user object cleanup callback. If specified, called upon "this" param's + * destruction (including on error). + */ + ast_aeap_user_obj_cleanup obj_cleanup; +}; + +/*! + * \brief Send a transaction based message to an external application using the given parameters + * + * \note "Steals" the given message reference, thus callers are not required to un-ref + * the message object after calling this function. + * + * \note Also handles cleaning up the user object if the obj_cleanup callback + * is specified in "params". + * + * \param aeap An Asterisk external application object + * \param msg The message to send + * \param params (optional) Additional parameters to consider when sending. Heap allocation + * not required. + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_send_msg_tsx(struct ast_aeap *aeap, struct ast_aeap_tsx_params *params); + +#endif /* AST_RES_AEAP_H */ diff --git a/include/asterisk/res_aeap_message.h b/include/asterisk/res_aeap_message.h new file mode 100644 index 0000000000000000000000000000000000000000..294039e41a588b2ebf1398d9185a8c75ed7ae500 --- /dev/null +++ b/include/asterisk/res_aeap_message.h @@ -0,0 +1,374 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*! \file + * \brief Asterisk External Application Protocol Message API + */ + +#ifndef AST_AEAP_MESSAGE_H +#define AST_AEAP_MESSAGE_H + +#include <stdint.h> + +#include "asterisk/res_aeap.h" + +struct ast_aeap_message; + +/*! + * \brief Message type virtual method table + */ +struct ast_aeap_message_type { + /*! The size of the message implementation type. Used for allocations. */ + size_t type_size; + /*! The name of this type */ + const char *type_name; + /*! The type to serialize to, and de-serialize from */ + enum AST_AEAP_DATA_TYPE serial_type; + + /*! + * \brief Construct/Initialize a message object + * + * \param self The message object to initialize + * \param params Other optional parameter(s) to possibly use + * + * \returns 0 on success, -1 on error + */ + int (*construct1)(struct ast_aeap_message *self, const void *params); + + /*! + * \brief Construct/Initialize a message object + * + * \param self The message object to initialize + * \param msg_type The type of message (e.g. request or response) + * \param name The name of the message + * \param id The message id + * \param params Other optional parameter(s) to possibly use + * + * \returns 0 on success, -1 on error + */ + int (*construct2)(struct ast_aeap_message *self, const char *msg_type, const char *name, + const char *id, const void *params); + + /*! + * \brief Destruct/Cleanup object resources + * + * \param self The message object being destructed + */ + void (*destruct)(struct ast_aeap_message *self); + + /*! + * \brief Deserialize the given buffer into a message object + * + * \param self The message object to deserialize into + * \param buf The buffer to deserialize + * \param size The size/length of the buffer + * + * \returns 0 on success, -1 on error + */ + int (*deserialize)(struct ast_aeap_message *self, const void *buf, intmax_t size); + + /*! + * \brief Serialize the message object into byte/char buffer + * + * \param self The message object to serialize + * \param buf [out] The buffer to hold the "packed" data + * \param size [out] The size of the data written to the buffer + * + * \returns 0 on success, -1 on error + */ + int (*serialize)(const struct ast_aeap_message *self, void **buf, intmax_t *size); + + /*! + * \brief Retrieve a message id + * + * \param self The message object + * + * \returns The message id + */ + const char *(*id)(const struct ast_aeap_message *self); + + /*! + * \brief Set a message id. + * + * \param self The message object + * \param id The id to set + * + * \returns 0 on success, -1 on error + */ + int (*id_set)(struct ast_aeap_message *self, const char *id); + + /*! + * \brief Retrieve a message name + * + * \param self The message object + * + * \returns The message name + */ + const char *(*name)(const struct ast_aeap_message *self); + + /*! + * \brief Retrieve the core message data/body + * + * \param self This message object + */ + void *(*data)(struct ast_aeap_message *self); + + /*! + * \brief Retrieve whether or not this is a request message + * + * \param self The message object + * + * \returns True if message is a request, false otherwise + */ + int (*is_request)(const struct ast_aeap_message *self); + + /*! + * \brief Retrieve whether or not this is a response message + * + * \param self The message object + * + * \returns True if message is a response, false otherwise + */ + int (*is_response)(const struct ast_aeap_message *self); + + /*! + * \brief Retrieve the error message if it has one + * + * \param self The message object + * + * \returns The error message if available, or NULL + */ + const char *(*error_msg)(const struct ast_aeap_message *self); + + /*! + * \brief Set an error message + * + * \param self The message object + * \param error_msg The error message string to set + * + * \returns 0 on success, -1 on error + */ + int (*error_msg_set)(struct ast_aeap_message *self, const char *error_msg); +}; + +/*! + * \brief Asterisk external application base message + */ +struct ast_aeap_message { + /*! The type virtual table */ + const struct ast_aeap_message_type *type; +}; + +/*! + * \brief Retrieve the serial type a message type + * + * \param type A message type + * + * \returns The type's serial type + */ +enum AST_AEAP_DATA_TYPE ast_aeap_message_serial_type(const struct ast_aeap_message_type *type); + +/*! + * \brief Create an Asterisk external application message object + * + * \param type The type of message object to create + * \param params Any parameter(s) to pass to the type's constructor + * + * \returns An ao2 reference counted AEAP message object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_create1(const struct ast_aeap_message_type *type, + const void *params); + +/*! + * \brief Create an Asterisk external application message object + * + * \param type The type of message object to create + * \param msg_type The type of message (e.g. request or response) + * \param name The name of the message + * \param id The message id + * \param params Other optional parameter(s) to possibly use + * + * \returns An ao2 reference counted AEAP message object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_create2(const struct ast_aeap_message_type *type, + const char *msg_type, const char *name, const char *id, const void *params); + +/*! + * \brief Create an Asterisk external application request object + * + * \param type The type of message object to create + * \param name The name of the message + * \param id Optional id (if NULL an id is generated) + * \param params Other optional parameter(s) to possibly use + * + * \returns An ao2 reference counted AEAP request object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_create_request(const struct ast_aeap_message_type *type, + const char *name, const char *id, const void *params); + +/*! + * \brief Create an Asterisk external application response object + * + * \param type The type of message object to create + * \param name The name of the message + * \param id Optional id + * \param params Other optional parameter(s) to possibly use + * + * \returns An ao2 reference counted AEAP response object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_create_response(const struct ast_aeap_message_type *type, + const char *name, const char *id, const void *params); + +/*! + * \brief Create an Asterisk external application error response object + * + * \param type The type of message object to create + * \param name The name of the message + * \param id Optional id + * \param error_msg Error message to set + * \param params Other optional parameter(s) to possibly use + * + * \returns An ao2 reference counted AEAP response object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_create_error(const struct ast_aeap_message_type *type, + const char *name, const char *id, const char *error_msg); + +/*! + * \brief Deserialize the given buffer into an Asterisk external application message object + * + * \param type The message type to create, and deserialize to + * \param buf The buffer to deserialize + * \param size The size/length of the buffer + * + * \returns An ao2 reference counted AEAP message object, or NULL on error + */ +struct ast_aeap_message *ast_aeap_message_deserialize(const struct ast_aeap_message_type *type, + const void *buf, intmax_t size); + +/*! + * \brief Serialize the given message object into a byte/char buffer + * + * \param message The message object to serialize + * \param buf [out] The buffer to hold the "packed" data + * \param size [out] The size of the data written to the buffer + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_message_serialize(const struct ast_aeap_message *message, + void **buf, intmax_t *size); + +/*! + * \brief Retrieve a message id + * + * \param message A message object + * + * \returns The message id, or an empty string + */ +const char *ast_aeap_message_id(const struct ast_aeap_message *message); + +/*! + * \brief Set a message id. + * + * \param message A message object + * \param id The id to set + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_message_id_set(struct ast_aeap_message *message, const char *id); + +/*! + * \brief Generate an id, and set it for the message + * + * \param message A message object + * + * \returns the generated id on success, or NULL on error + */ +const char *ast_aeap_message_id_generate(struct ast_aeap_message *message); + +/*! + * \brief Retrieve a message name + * + * \param message A message object + * + * \returns The message name, or an empty string + */ +const char *ast_aeap_message_name(const struct ast_aeap_message *message); + +/*! + * \brief Check whether or not a message's name matches the given one + * + * \note Case insensitive + * + * \param message A message object + * \param message name The name to check against + * + * \returns True if matched, false otherwise + */ +int ast_aeap_message_is_named(const struct ast_aeap_message *message, const char *name); + +/*! + * \brief Retrieve the core message data/body + * + * \param message A message object + */ +void *ast_aeap_message_data(struct ast_aeap_message *message); + +/*! + * \brief Retrieve whether or not this is a request message + * + * \param message A message object + * + * \returns True if the message is a request, false otherwise + */ +int ast_aeap_message_is_request(const struct ast_aeap_message *message); + +/*! + * \brief Retrieve whether or not this is a response message + * + * \param message A message object + * + * \returns True if the message is a response, false otherwise + */ +int ast_aeap_message_is_response(const struct ast_aeap_message *message); + +/*! + * \brief Retrieve the error message if it has one + * + * \param message A message object + * + * \returns The error message if available, or NULL + */ +const char *ast_aeap_message_error_msg(const struct ast_aeap_message *message); + +/*! + * \brief Set an error message. + * + * \param message A message object + * \param error_msg The error string to set + * + * \returns 0 on success, -1 on error + */ +int ast_aeap_message_error_msg_set(struct ast_aeap_message *message, + const char *error_msg); + +/*! + * \brief Asterisk external application JSON message type + */ +extern const struct ast_aeap_message_type *ast_aeap_message_type_json; + +#endif /* AST_AEAP_MESSAGE_H */ diff --git a/include/asterisk/speech.h b/include/asterisk/speech.h index 681c536019c1f67f031d3f9d8f1be96e1849503f..ec5abaf3c382c7f9bf714d582fab04037f192aa8 100644 --- a/include/asterisk/speech.h +++ b/include/asterisk/speech.h @@ -158,6 +158,12 @@ int ast_speech_unregister(const char *engine_name); /*! \brief Unregister a speech recognition engine */ struct ast_speech_engine *ast_speech_unregister2(const char *engine_name); +/*! \brief Retrieve a speech recognition engine */ +struct ast_speech_engine *ast_speech_find_engine(const char *engine_name); +/*! \brief Unregister all speech recognition engines told to by callback */ +void ast_speech_unregister_engines( + int (*should_unregister)(const struct ast_speech_engine *engine, void *data), void *data, + void (*on_unregistered)(void *obj)); #if defined(__cplusplus) || defined(c_plusplus) } diff --git a/res/Makefile b/res/Makefile index 45ef749a3f2c1660b1d8643f28ee61b546ec507d..d54130011faf702a422dc6b893b83b161ab87be6 100644 --- a/res/Makefile +++ b/res/Makefile @@ -69,6 +69,7 @@ $(call MOD_ADD_C,res_ari,ari/cli.c ari/config.c ari/ari_websockets.c) $(call MOD_ADD_C,res_ari_model,ari/ari_model_validators.c) $(call MOD_ADD_C,res_stasis_recording,stasis_recording/stored.c) $(call MOD_ADD_C,res_stir_shaken,$(wildcard res_stir_shaken/*.c)) +$(call MOD_ADD_C,res_aeap,$(wildcard res_aeap/*.c)) res_parking.o: _ASTCFLAGS+=$(AST_NO_FORMAT_TRUNCATION) snmp/agent.o: _ASTCFLAGS+=-fPIC diff --git a/res/res_aeap.c b/res/res_aeap.c index b6c584109fab9d6a0198d1148f0913de87f04b36..e78956e20b7ed9454e110a95dad3248c22a45ecf 100644 --- a/res/res_aeap.c +++ b/res/res_aeap.c @@ -17,29 +17,37 @@ */ /*** MODULEINFO + <depend>res_http_websocket</depend> <support_level>core</support_level> ***/ #include "asterisk.h" +#include "asterisk/astobj2.h" #include "asterisk/module.h" #include "asterisk/sorcery.h" #include "asterisk/cli.h" #include "asterisk/format.h" #include "asterisk/format_cap.h" +#include "asterisk/res_aeap.h" + +#include "res_aeap/general.h" /*** DOCUMENTATION <configInfo name="res_aeap" language="en_US"> <synopsis>Asterisk External Application Protocol (AEAP) module for Asterisk</synopsis> <configFile name="aeap.conf"> - <configObject name="server"> - <synopsis>AEAP server options</synopsis> + <configObject name="client"> + <synopsis>AEAP client options</synopsis> <configOption name="type"> - <synopsis>Must be of type 'server'.</synopsis> + <synopsis>Must be of type 'client'.</synopsis> </configOption> - <configOption name="server_url"> + <configOption name="url"> <synopsis>The URL of the server to connect to.</synopsis> </configOption> + <configOption name="protocol"> + <synopsis>The application protocol.</synopsis> + </configOption> <configOption name="codecs"> <synopsis>Optional media codec(s)</synopsis> <description><para> @@ -56,30 +64,36 @@ /* Asterisk External Application Protocol sorcery object */ static struct ast_sorcery *aeap_sorcery; -struct aeap_server +struct ast_sorcery *ast_aeap_sorcery(void) { + return aeap_sorcery; +} + +struct ast_aeap_client_config { SORCERY_OBJECT(details); AST_DECLARE_STRING_FIELDS( /*! The URL of the server to connect to */ - AST_STRING_FIELD(server_url); + AST_STRING_FIELD(url); + /*! The application protocol */ + AST_STRING_FIELD(protocol); ); /*! An optional list of codecs that will be used if provided */ struct ast_format_cap *codecs; }; -static void aeap_server_destructor(void *obj) +static void client_config_destructor(void *obj) { - struct aeap_server *cfg = obj; + struct ast_aeap_client_config *cfg = obj; ast_string_field_free_memory(cfg); ao2_cleanup(cfg->codecs); } -static void *aeap_server_alloc(const char *name) +static void *client_config_alloc(const char *name) { - struct aeap_server *cfg; + struct ast_aeap_client_config *cfg; - cfg = ast_sorcery_generic_alloc(sizeof(*cfg), aeap_server_destructor); + cfg = ast_sorcery_generic_alloc(sizeof(*cfg), client_config_destructor); if (!cfg) { return NULL; } @@ -97,32 +111,52 @@ static void *aeap_server_alloc(const char *name) return cfg; } -static int aeap_server_apply(const struct ast_sorcery *sorcery, void *obj) +static int client_config_apply(const struct ast_sorcery *sorcery, void *obj) { - struct aeap_server *cfg = obj; + struct ast_aeap_client_config *cfg = obj; - if (ast_strlen_zero(cfg->server_url)) { - ast_log(LOG_ERROR, "AEAP - Server URL must be present for server '%s'\n", ast_sorcery_object_get_id(cfg)); + if (ast_strlen_zero(cfg->url)) { + ast_log(LOG_ERROR, "AEAP - URL must be present for '%s'\n", ast_sorcery_object_get_id(cfg)); return -1; } - if (!ast_begins_with(cfg->server_url, "ws")) { - ast_log(LOG_ERROR, "AEAP - Server URL must be ws or wss for server '%s'\n", ast_sorcery_object_get_id(cfg)); + if (!ast_begins_with(cfg->url, "ws")) { + ast_log(LOG_ERROR, "AEAP - URL must be ws or wss for '%s'\n", ast_sorcery_object_get_id(cfg)); return -1; } return 0; } -static struct aeap_server *aeap_server_get(const char *id) +const struct ast_format_cap *ast_aeap_client_config_codecs(const struct ast_aeap_client_config *cfg) { - return ast_sorcery_retrieve_by_id(aeap_sorcery, "server", id); + return cfg->codecs; } -static struct ao2_container *aeap_server_get_all(void) +int ast_aeap_client_config_has_protocol(const struct ast_aeap_client_config *cfg, + const char *protocol) { - return ast_sorcery_retrieve_by_fields(aeap_sorcery, "server", - AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + return !strcmp(protocol, cfg->protocol); +} + +struct ao2_container *ast_aeap_client_configs_get(const char *protocol) +{ + struct ao2_container *container; + struct ast_variable *var; + + var = protocol ? ast_variable_new("protocol ==", protocol, "") : NULL; + + container = ast_sorcery_retrieve_by_fields(aeap_sorcery, + AEAP_CONFIG_CLIENT, AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, var); + + ast_variables_destroy(var); + + return container; +} + +static struct ast_aeap_client_config *client_config_get(const char *id) +{ + return ast_sorcery_retrieve_by_id(aeap_sorcery, AEAP_CONFIG_CLIENT, id); } static char *aeap_tab_complete_name(const char *word, struct ao2_container *container) @@ -145,6 +179,8 @@ static char *aeap_tab_complete_name(const char *word, struct ao2_container *cont } ao2_iterator_destroy(&it); + ao2_ref(container, -1); + return NULL; } @@ -159,8 +195,7 @@ static int aeap_cli_show(void *obj, void *arg, int flags) return 0; } - options = ast_variable_list_sort(ast_sorcery_objectset_create2( - aeap_sorcery, obj, AST_HANDLER_ONLY_STRING)); + options = ast_variable_list_sort(ast_sorcery_objectset_create(aeap_sorcery, obj)); if (!options) { return 0; } @@ -179,20 +214,20 @@ static int aeap_cli_show(void *obj, void *arg, int flags) return 0; } -static char *aeap_server_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +static char *client_config_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { - struct aeap_server *cfg; + struct ast_aeap_client_config *cfg; switch(cmd) { case CLI_INIT: - e->command = "aeap show server"; + e->command = "aeap show client"; e->usage = - "Usage: aeap show server <id>\n" - " Show the AEAP settings for a given server\n"; + "Usage: aeap show client <id>\n" + " Show the AEAP settings for a given client\n"; return NULL; case CLI_GENERATE: if (a->pos == 3) { - return aeap_tab_complete_name(a->word, aeap_server_get_all()); + return aeap_tab_complete_name(a->word, ast_aeap_client_configs_get(NULL)); } else { return NULL; } @@ -202,23 +237,23 @@ static char *aeap_server_show(struct ast_cli_entry *e, int cmd, struct ast_cli_a return CLI_SHOWUSAGE; } - cfg = aeap_server_get(a->argv[3]); + cfg = client_config_get(a->argv[3]); aeap_cli_show(cfg, a, 0); ao2_cleanup(cfg); return CLI_SUCCESS; } -static char *aeap_server_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +static char *client_config_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { struct ao2_container *container; switch(cmd) { case CLI_INIT: - e->command = "aeap show servers"; + e->command = "aeap show clients"; e->usage = - "Usage: aeap show servers\n" - " Show all configured AEAP servers\n"; + "Usage: aeap show clients\n" + " Show all configured AEAP clients\n"; return NULL; case CLI_GENERATE: return NULL; @@ -228,9 +263,9 @@ static char *aeap_server_show_all(struct ast_cli_entry *e, int cmd, struct ast_c return CLI_SHOWUSAGE; } - container = aeap_server_get_all(); + container = ast_aeap_client_configs_get(NULL); if (!container || ao2_container_count(container) == 0) { - ast_cli(a->fd, "No AEAP servers found\n"); + ast_cli(a->fd, "No AEAP clients found\n"); ao2_cleanup(container); return CLI_SUCCESS; } @@ -242,12 +277,75 @@ static char *aeap_server_show_all(struct ast_cli_entry *e, int cmd, struct ast_c } static struct ast_cli_entry aeap_cli[] = { - AST_CLI_DEFINE(aeap_server_show, "Show AEAP server configuration by id"), - AST_CLI_DEFINE(aeap_server_show_all, "Show all AEAP server configurations"), + AST_CLI_DEFINE(client_config_show, "Show AEAP client configuration by id"), + AST_CLI_DEFINE(client_config_show_all, "Show all AEAP client configurations"), }; +static struct ast_aeap *aeap_create(const char *id, const struct ast_aeap_params *params, + int connect, int timeout) +{ + struct ast_aeap_client_config *cfg; + struct ast_aeap *aeap; + const char *url = NULL; + const char *protocol = NULL; + + cfg = client_config_get(id); + if (cfg) { + url = cfg->url; + protocol = cfg->protocol; + } + +#ifdef TEST_FRAMEWORK + else if (ast_begins_with(id, "_aeap_test_")) { + url = "ws://127.0.0.1:8088/ws"; + protocol = id; + } +#endif + + if (!url && !protocol) { + ast_log(LOG_ERROR, "AEAP: unable to get configuration for '%s'\n", id); + return NULL; + } + + aeap = connect ? ast_aeap_create_and_connect(url, params, url, protocol, timeout) : + ast_aeap_create(url, params); + + ao2_cleanup(cfg); + return aeap; +} + +struct ast_aeap *ast_aeap_create_by_id(const char *id, const struct ast_aeap_params *params) +{ + return aeap_create(id, params, 0, 0); +} + +struct ast_aeap *ast_aeap_create_and_connect_by_id(const char *id, + const struct ast_aeap_params *params, int timeout) +{ + return aeap_create(id, params, 1, timeout); +} + +struct ast_variable *ast_aeap_custom_fields_get(const char *id) +{ + struct ast_aeap_client_config *cfg; + struct ast_variable *vars; + + cfg = client_config_get(id); + if (!cfg) { + ast_log(LOG_WARNING, "AEAP: no client configuration '%s' to get fields\n", id); + return NULL; + } + + vars = ast_sorcery_objectset_create(aeap_sorcery, cfg); + + ao2_ref(cfg, -1); + return vars; +} + static int reload_module(void) { + ast_sorcery_reload(aeap_sorcery); + return 0; } @@ -258,28 +356,35 @@ static int unload_module(void) ast_cli_unregister_multiple(aeap_cli, ARRAY_LEN(aeap_cli)); + aeap_general_finalize(); + return 0; } static int load_module(void) { + if (aeap_general_initialize()) { + return AST_MODULE_LOAD_DECLINE; + } + if (!(aeap_sorcery = ast_sorcery_open())) { ast_log(LOG_ERROR, "AEAP - failed to open sorcery\n"); return AST_MODULE_LOAD_DECLINE; } - ast_sorcery_apply_default(aeap_sorcery, "server", "config", "aeap.conf,criteria=type=server"); + ast_sorcery_apply_default(aeap_sorcery, AEAP_CONFIG_CLIENT, "config", "aeap.conf,criteria=type=client"); - if (ast_sorcery_object_register(aeap_sorcery, "server", aeap_server_alloc, - NULL, aeap_server_apply)) { - ast_log(LOG_ERROR, "AEAP - failed to register server sorcery object\n"); + if (ast_sorcery_object_register(aeap_sorcery, "client", client_config_alloc, + NULL, client_config_apply)) { + ast_log(LOG_ERROR, "AEAP - failed to register client sorcery object\n"); return AST_MODULE_LOAD_DECLINE; } - ast_sorcery_object_field_register(aeap_sorcery, "server", "type", "", OPT_NOOP_T, 0, 0); - ast_sorcery_object_field_register(aeap_sorcery, "server", "server_url", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct aeap_server, server_url)); - ast_sorcery_object_field_register(aeap_sorcery, "server", "codecs", "", OPT_CODEC_T, 1, FLDSET(struct aeap_server, codecs)); + ast_sorcery_object_field_register(aeap_sorcery, AEAP_CONFIG_CLIENT, "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(aeap_sorcery, AEAP_CONFIG_CLIENT, "url", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_aeap_client_config, url)); + ast_sorcery_object_field_register(aeap_sorcery, AEAP_CONFIG_CLIENT, "protocol", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_aeap_client_config, protocol)); + ast_sorcery_object_field_register(aeap_sorcery, AEAP_CONFIG_CLIENT, "codecs", "", OPT_CODEC_T, 1, FLDSET(struct ast_aeap_client_config, codecs)); ast_sorcery_load(aeap_sorcery); @@ -295,4 +400,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ .unload = unload_module, .reload = reload_module, .load_pri = AST_MODPRI_CHANNEL_DEPEND, + .requires = "res_http_websocket", ); diff --git a/res/res_aeap.exports.in b/res/res_aeap.exports.in new file mode 100644 index 0000000000000000000000000000000000000000..c1fc5c4988505c157cc29bd2f1772a55f3735a69 --- /dev/null +++ b/res/res_aeap.exports.in @@ -0,0 +1,7 @@ +{ + global: + LINKER_SYMBOL_PREFIXaeap_*; + LINKER_SYMBOL_PREFIXast_aeap_*; + local: + *; +}; diff --git a/res/res_aeap/aeap.c b/res/res_aeap/aeap.c new file mode 100644 index 0000000000000000000000000000000000000000..9094bbb3dbbfaacf3f19166812399a28f5147f32 --- /dev/null +++ b/res/res_aeap/aeap.c @@ -0,0 +1,501 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 <pthread.h> + +#include "asterisk/astobj2.h" +#include "asterisk/strings.h" + +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#include "logger.h" +#include "transaction.h" +#include "transport.h" + +#define AEAP_RECV_SIZE 32768 + +struct aeap_user_data { + /*! The user data object */ + void *obj; + /*! A user data identifier */ + char id[0]; +}; + +AO2_STRING_FIELD_HASH_FN(aeap_user_data, id); +AO2_STRING_FIELD_CMP_FN(aeap_user_data, id); + +#define USER_DATA_BUCKETS 11 + +struct ast_aeap { + /*! This object's configuration parameters */ + const struct ast_aeap_params *params; + /*! Container for registered user data objects */ + struct ao2_container *user_data; + /*! Transactions container */ + struct ao2_container *transactions; + /*! Transport layer communicator */ + struct aeap_transport *transport; + /*! Id of thread that reads data from the transport */ + pthread_t read_thread_id; +}; + +static int tsx_end(void *obj, void *arg, int flags) +{ + aeap_transaction_end(obj, -1); + + return 0; +} + +static void aeap_destructor(void *obj) +{ + struct ast_aeap *aeap = obj; + + /* Disconnect things first, which keeps transactions from further executing */ + ast_aeap_disconnect(aeap); + + aeap_transport_destroy(aeap->transport); + + /* + * Each contained transaction holds a pointer back to this transactions container, + * which is removed upon transaction end. Thus by explicitly ending each transaction + * here we can ensure all references to the transactions container are removed. + */ + ao2_callback(aeap->transactions, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, + tsx_end, NULL); + ao2_cleanup(aeap->transactions); + + ao2_cleanup(aeap->user_data); +} + +struct ast_aeap *ast_aeap_create(const char *transport_type, + const struct ast_aeap_params *params) +{ + struct ast_aeap *aeap; + + aeap = ao2_alloc(sizeof(*aeap), aeap_destructor); + if (!aeap) { + ast_log(LOG_ERROR, "AEAP: unable to create"); + return NULL; + } + + aeap->params = params; + aeap->read_thread_id = AST_PTHREADT_NULL; + + aeap->user_data = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, USER_DATA_BUCKETS, + aeap_user_data_hash_fn, NULL, aeap_user_data_cmp_fn); + if (!aeap->user_data) { + aeap_error(aeap, NULL, "unable to create user data container"); + ao2_ref(aeap, -1); + return NULL; + } + + aeap->transactions = aeap_transactions_create(); + if (!aeap->transactions) { + aeap_error(aeap, NULL, "unable to create transactions container"); + ao2_ref(aeap, -1); + return NULL; + } + + aeap->transport = aeap_transport_create(transport_type); + if (!aeap->transport) { + aeap_error(aeap, NULL, "unable to create transport"); + ao2_ref(aeap, -1); + return NULL; + } + + return aeap; +} + +static struct aeap_user_data *aeap_user_data_create(const char *id, void *obj, + ast_aeap_user_obj_cleanup cleanup) +{ + struct aeap_user_data *data; + + ast_assert(id != NULL); + + data = ao2_t_alloc_options(sizeof(*data) + strlen(id) + 1, cleanup, + AO2_ALLOC_OPT_LOCK_NOLOCK, ""); + if (!data) { + if (cleanup) { + cleanup(obj); + } + + return NULL; + } + + strcpy(data->id, id); /* safe */ + data->obj = obj; + + return data; +} + +int ast_aeap_user_data_register(struct ast_aeap *aeap, const char *id, void *obj, + ast_aeap_user_obj_cleanup cleanup) +{ + struct aeap_user_data *data; + + data = aeap_user_data_create(id, obj, cleanup); + if (!data) { + return -1; + } + + if (!ao2_link(aeap->user_data, data)) { + ao2_ref(data, -1); + return -1; + } + + ao2_ref(data, -1); + return 0; +} + +void ast_aeap_user_data_unregister(struct ast_aeap *aeap, const char *id) +{ + ao2_find(aeap->user_data, id, OBJ_SEARCH_KEY | OBJ_UNLINK | OBJ_NODATA); +} + +void *ast_aeap_user_data_object_by_id(struct ast_aeap *aeap, const char *id) +{ + struct aeap_user_data *data; + void *obj; + + data = ao2_find(aeap->user_data, id, OBJ_SEARCH_KEY); + if (!data) { + return NULL; + } + + obj = data->obj; + ao2_ref(data, -1); + + /* + * Returned object's lifetime is based on how it was registered. + * See public function docs for more info + */ + return obj; +} + +static int raise_msg_handler(struct ast_aeap *aeap, const struct ast_aeap_message_handler *handlers, + size_t size, struct ast_aeap_message *msg, void *data) +{ + ast_aeap_on_message on_message = NULL; + size_t i; + + if (!aeap->params->emit_error) { + const char *error_msg = ast_aeap_message_error_msg(msg); + + if (error_msg) { + aeap_error(aeap, NULL, "%s", error_msg); + return -1; + } + + /* If no error_msg then it's assumed this is not an error message */ + } + + for (i = 0; i < size; ++i) { + if (ast_strlen_zero(handlers[i].name)) { + /* A default handler is specified. Use it if no other match is found */ + on_message = handlers[i].on_message; + continue; + } + + if (ast_aeap_message_is_named(msg, handlers[i].name)) { + on_message = handlers[i].on_message; + break; + } + } + + if (on_message) { + return on_message(aeap, msg, data); + } + + /* Respond with un-handled error */ + ast_aeap_send_msg(aeap, ast_aeap_message_create_error(aeap->params->msg_type, + ast_aeap_message_name(msg), ast_aeap_message_id(msg), + "Unsupported and/or un-handled message")); + + return 0; +} + +static void raise_msg(struct ast_aeap *aeap, const void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE serial_type) +{ + struct ast_aeap_message *msg; + struct aeap_transaction *tsx; + int res = 0; + + if (!aeap->params || !aeap->params->msg_type || + ast_aeap_message_serial_type(aeap->params->msg_type) != serial_type || + !(msg = ast_aeap_message_deserialize(aeap->params->msg_type, buf, size))) { + return; + } + + /* See if this msg is involved in a transaction */ + tsx = aeap_transaction_get(aeap->transactions, ast_aeap_message_id(msg)); + + /* If so go ahead and cancel the timeout timer */ + aeap_transaction_cancel_timer(tsx); + + if (aeap->params->request_handlers && ast_aeap_message_is_request(msg)) { + res = raise_msg_handler(aeap, aeap->params->request_handlers, aeap->params->request_handlers_size, + msg, tsx ? aeap_transaction_user_obj(tsx) : NULL); + } else if (aeap->params->response_handlers && ast_aeap_message_is_response(msg)) { + res = raise_msg_handler(aeap, aeap->params->response_handlers, aeap->params->response_handlers_size, + msg, tsx ? aeap_transaction_user_obj(tsx) : NULL); + } + + /* Complete transaction (Note, removes tsx ref) */ + aeap_transaction_end(tsx, res); + + ao2_ref(msg, -1); +} + +static void *aeap_receive(void *data) +{ + struct ast_aeap *aeap = data; + void *buf; + + buf = ast_calloc(1, AEAP_RECV_SIZE); + if (!buf) { + aeap_error(aeap, NULL, "unable to create read buffer"); + goto aeap_receive_error; + } + + while (aeap_transport_is_connected(aeap->transport)) { + enum AST_AEAP_DATA_TYPE rtype; + intmax_t size; + + size = aeap_transport_read(aeap->transport, buf, AEAP_RECV_SIZE, &rtype); + if (size < 0) { + goto aeap_receive_error; + } + + if (!size) { + continue; + } + + switch (rtype) { + case AST_AEAP_DATA_TYPE_BINARY: + if (aeap->params && aeap->params->on_binary) { + aeap->params->on_binary(aeap, buf, size); + } + break; + case AST_AEAP_DATA_TYPE_STRING: + ast_debug(3, "AEAP: received message: %s\n", (char *)buf); + if (aeap->params && aeap->params->on_string) { + aeap->params->on_string(aeap, (const char *)buf, size - 1); + } + break; + default: + break; + } + + raise_msg(aeap, buf, size, rtype); + }; + + ast_free(buf); + return NULL; + +aeap_receive_error: + /* + * An unrecoverable error occurred so ensure the aeap and transport reset + * to a disconnected state. We don't want this thread to "join" itself so set + * its id to NULL prior to disconnecting. + */ + aeap_error(aeap, NULL, "unrecoverable read error, disconnecting"); + + ao2_lock(aeap); + aeap->read_thread_id = AST_PTHREADT_NULL; + ao2_unlock(aeap); + + ast_aeap_disconnect(aeap); + + ast_free(buf); + + if (aeap->params && aeap->params->on_error) { + aeap->params->on_error(aeap); + } + + return NULL; +} + +int ast_aeap_connect(struct ast_aeap *aeap, const char *url, const char *protocol, int timeout) +{ + SCOPED_AO2LOCK(lock, aeap); + + if (aeap_transport_is_connected(aeap->transport)) { + /* Should already be connected, so nothing to do */ + return 0; + } + + if (aeap_transport_connect(aeap->transport, url, protocol, timeout)) { + aeap_error(aeap, NULL, "unable to connect transport"); + return -1; + } + + if (ast_pthread_create_background(&aeap->read_thread_id, NULL, + aeap_receive, aeap)) { + aeap_error(aeap, NULL, "unable to start read thread: %s", + strerror(errno)); + ast_aeap_disconnect(aeap); + return -1; + } + + return 0; +} + +struct ast_aeap *ast_aeap_create_and_connect(const char *type, + const struct ast_aeap_params *params, const char *url, const char *protocol, int timeout) +{ + struct ast_aeap *aeap; + + aeap = ast_aeap_create(type, params); + if (!aeap) { + return NULL; + } + + if (ast_aeap_connect(aeap, url, protocol, timeout)) { + ao2_ref(aeap, -1); + return NULL; + } + + return aeap; +} + +int ast_aeap_disconnect(struct ast_aeap *aeap) +{ + ao2_lock(aeap); + + aeap_transport_disconnect(aeap->transport); + + if (aeap->read_thread_id != AST_PTHREADT_NULL) { + /* + * The read thread calls disconnect if an error occurs, so + * unlock the aeap before "joining" to avoid a deadlock. + */ + ao2_unlock(aeap); + pthread_join(aeap->read_thread_id, NULL); + ao2_lock(aeap); + + aeap->read_thread_id = AST_PTHREADT_NULL; + } + + ao2_unlock(aeap); + + return 0; +} + +static int aeap_send(struct ast_aeap *aeap, const void *buf, uintmax_t size, + enum AST_AEAP_DATA_TYPE type) +{ + intmax_t num; + + num = aeap_transport_write(aeap->transport, buf, size, type); + + if (num == 0) { + /* Nothing written, could be disconnected */ + return 0; + } + + if (num < 0) { + aeap_error(aeap, NULL, "error sending data"); + return -1; + } + + if (num < size) { + aeap_error(aeap, NULL, "not all data sent"); + return -1; + } + + if (num > size) { + aeap_error(aeap, NULL, "sent data truncated"); + return -1; + } + + return 0; +} + +int ast_aeap_send_binary(struct ast_aeap *aeap, const void *buf, uintmax_t size) +{ + return aeap_send(aeap, buf, size, AST_AEAP_DATA_TYPE_BINARY); +} + +int ast_aeap_send_msg(struct ast_aeap *aeap, struct ast_aeap_message *msg) +{ + void *buf; + intmax_t size; + int res; + + if (!msg) { + aeap_error(aeap, NULL, "no message to send"); + return -1; + } + + if (ast_aeap_message_serialize(msg, &buf, &size)) { + aeap_error(aeap, NULL, "unable to serialize outgoing message"); + ao2_ref(msg, -1); + return -1; + } + + res = aeap_send(aeap, buf, size, msg->type->serial_type); + + ast_free(buf); + ao2_ref(msg, -1); + + return res; +} + +int ast_aeap_send_msg_tsx(struct ast_aeap *aeap, struct ast_aeap_tsx_params *params) +{ + struct aeap_transaction *tsx = NULL; + int res = 0; + + if (!params) { + return -1; + } + + if (!params->msg) { + aeap_transaction_params_cleanup(params); + aeap_error(aeap, NULL, "no message to send"); + return -1; + } + + /* The transaction will take over params cleanup, which includes the msg reference */ + tsx = aeap_transaction_create_and_add(aeap->transactions, + ast_aeap_message_id(params->msg), params, aeap); + if (!tsx) { + return -1; + } + + if (ast_aeap_send_msg(aeap, ao2_bump(params->msg))) { + aeap_transaction_end(tsx, -1); /* Removes container, and tsx ref */ + return -1; + } + + if (aeap_transaction_start(tsx)) { + aeap_transaction_end(tsx, -1); /* Removes container, and tsx ref */ + return -1; + } + + res = aeap_transaction_result(tsx); + + ao2_ref(tsx, -1); + + return res; +} diff --git a/res/res_aeap/general.c b/res/res_aeap/general.c new file mode 100644 index 0000000000000000000000000000000000000000..7bd180703fb4de4cdf82774936781b97561e57ef --- /dev/null +++ b/res/res_aeap/general.c @@ -0,0 +1,58 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/astobj2.h" +#include "asterisk/sched.h" + +#include "general.h" + +/*! \brief Scheduler for transaction timeouts */ +static struct ast_sched_context *sched = NULL; + +struct ast_sched_context *aeap_sched_context(void) +{ + return sched; +} + +void aeap_general_finalize(void) +{ + if (sched) { + ast_sched_context_destroy(sched); + sched = NULL; + } +} + +int aeap_general_initialize(void) +{ + sched = ast_sched_context_create(); + if (!sched) { + ast_log(LOG_ERROR, "AEAP scheduler: unable to create context"); + return -1; + } + + if (ast_sched_start_thread(sched)) { + ast_log(LOG_ERROR, "AEAP scheduler: unable to start thread"); + aeap_general_finalize(); + return -1; + } + + return 0; +} + diff --git a/res/res_aeap/general.h b/res/res_aeap/general.h new file mode 100644 index 0000000000000000000000000000000000000000..52a092b43c3dd79f8a05bf83ca787e99cc03735e --- /dev/null +++ b/res/res_aeap/general.h @@ -0,0 +1,41 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +#ifndef RES_AEAP_GENERAL_H +#define RES_AEAP_GENERAL_H + +/*! + * \brief Retrieve the scheduling context + * + * \returns The scheduling context + */ +struct ast_sched_context *aeap_sched_context(void); + +/*! + * \brief Initialize general/common AEAP facilities + * + * \returns 0 on success, -1 on error + */ +int aeap_general_initialize(void); + +/*! + * \brief Finalize/cleanup general AEAP facilities + */ +void aeap_general_finalize(void); + +#endif /* RES_AEAP_GENERAL_H */ diff --git a/res/res_aeap/logger.h b/res/res_aeap/logger.h new file mode 100644 index 0000000000000000000000000000000000000000..db264c357fe84dc267ed3d8887c14fe5ec71bf60 --- /dev/null +++ b/res/res_aeap/logger.h @@ -0,0 +1,60 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +#ifndef RES_AEAP_LOGGER_H +#define RES_AEAP_LOGGER_H + +#include "asterisk.h" + +#include "asterisk/logger.h" +#include "asterisk/strings.h" + +/*! + * \brief Log an Asterisk external application message + * + * \param level The logging level + * \param obj The object being logged + * \param name Optional subsystem name + * \param fmt Format string + * \param ... Parameters for the format string + */ +#define aeap_log(level, obj, name, fmt, ...) \ + ast_log(level, "AEAP%s%s (%p): " fmt "\n", ast_strlen_zero(name) ? "" : " ", \ + ast_strlen_zero(name) ? "" : name, obj, ##__VA_ARGS__) + +/*! + * \brief Log an Asterisk external application error + * + * \param obj The object being logged + * \param name Optional subsystem name + * \param fmt Format string + * \param ... Parameters for the format string + */ +#define aeap_error(obj, name, fmt, ...) aeap_log(LOG_ERROR, obj, name, fmt, ##__VA_ARGS__) + +/*! + * \brief Log an Asterisk external application warning + * + * \param obj The object being logged + * \param name Optional subsystem name + * \param fmt Format string + * \param ... Parameters for the format string + */ +#define aeap_warn(obj, name, fmt, ...) aeap_log(LOG_WARNING, obj, name, fmt, ##__VA_ARGS__) + +#endif /* RES_AEAP_LOGGER_H */ diff --git a/res/res_aeap/message.c b/res/res_aeap/message.c new file mode 100644 index 0000000000000000000000000000000000000000..826e93b664ee3cc4a739aaf08978db93d3ed1a43 --- /dev/null +++ b/res/res_aeap/message.c @@ -0,0 +1,270 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/astobj2.h" +#include "asterisk/utils.h" +#include "asterisk/uuid.h" +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +enum AST_AEAP_DATA_TYPE ast_aeap_message_serial_type(const struct ast_aeap_message_type *type) +{ + ast_assert(type != NULL); + + return type->serial_type; +} + +static void message_destructor(void *obj) +{ + struct ast_aeap_message *msg = obj; + + if (msg->type->destruct) { + msg->type->destruct(msg); + } +} + +static struct ast_aeap_message *message_create(const struct ast_aeap_message_type *type) +{ + struct ast_aeap_message *msg; + + msg = ao2_t_alloc_options(type->type_size, message_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK, type->type_name); + if (!msg) { + ast_log(LOG_ERROR, "AEAP message %s: unable to create\n", type->type_name); + return NULL; + } + + msg->type = type; + + return msg; +} + +struct ast_aeap_message *ast_aeap_message_create1(const struct ast_aeap_message_type *type, + const void *params) +{ + struct ast_aeap_message *msg; + + ast_assert(type != NULL); + ast_assert(type->construct1 != NULL); + + msg = message_create(type); + if (!msg) { + return NULL; + } + + if (type->construct1(msg, params)) { + ast_log(LOG_ERROR, "AEAP message %s: unable to construct1\n", type->type_name); + ao2_ref(msg, -1); + return NULL; + } + + return msg; +} + +struct ast_aeap_message *ast_aeap_message_create2(const struct ast_aeap_message_type *type, + const char *msg_type, const char *name, const char *id, const void *params) +{ + struct ast_aeap_message *msg; + + ast_assert(type != NULL); + ast_assert(type->construct2 != NULL); + ast_assert(msg_type != NULL); + ast_assert(name != NULL); + + msg = message_create(type); + if (!msg) { + return NULL; + } + + if (type->construct2(msg, msg_type, name, id, params)) { + ast_log(LOG_ERROR, "AEAP message %s: unable to construct2\n", type->type_name); + ao2_ref(msg, -1); + return NULL; + } + + return msg; +} + +struct ast_aeap_message *ast_aeap_message_create_request(const struct ast_aeap_message_type *type, + const char *name, const char *id, const void *params) +{ + struct ast_aeap_message *msg; + + msg = ast_aeap_message_create2(type, "request", name, id, params); + if (!msg) { + return NULL; + } + + if (!id && !ast_aeap_message_id_generate(msg)) { + ao2_ref(msg, -1); + return NULL; + } + + return msg; +} + +struct ast_aeap_message *ast_aeap_message_create_response(const struct ast_aeap_message_type *type, + const char *name, const char *id, const void *params) +{ + return ast_aeap_message_create2(type, "response", name, id, params); +} + +struct ast_aeap_message *ast_aeap_message_create_error(const struct ast_aeap_message_type *type, + const char *name, const char *id, const char *error_msg) +{ + struct ast_aeap_message *msg; + + msg = ast_aeap_message_create_response(type, name, id, NULL); + if (!msg) { + return NULL; + } + + if (ast_aeap_message_error_msg_set(msg, error_msg)) { + ao2_ref(msg, -1); + return NULL; + } + + return msg; +} + +struct ast_aeap_message *ast_aeap_message_deserialize(const struct ast_aeap_message_type *type, + const void *buf, intmax_t size) +{ + struct ast_aeap_message *msg; + + ast_assert(type != NULL); + ast_assert(type->deserialize != NULL); + + msg = ast_aeap_message_create1(type, NULL); + if (!msg) { + return NULL; + } + + if (type->deserialize(msg, buf, size)) { + ao2_ref(msg, -1); + return NULL; + } + + return msg; +} + +int ast_aeap_message_serialize(const struct ast_aeap_message *message, + void **buf, intmax_t *size) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->serialize ? message->type->serialize(message, buf, size) : 0; +} + +const char *ast_aeap_message_id(const struct ast_aeap_message *message) +{ + const char *id = NULL; + + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + if (message->type->id) { + id = message->type->id(message); + } + + return id ? id : ""; +} + +int ast_aeap_message_id_set(struct ast_aeap_message *message, const char *id) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->id_set ? message->type->id_set(message, id) : 0; +} + +const char *ast_aeap_message_id_generate(struct ast_aeap_message *message) +{ + char uuid_str[AST_UUID_STR_LEN]; + + ast_uuid_generate_str(uuid_str, sizeof(uuid_str)); + if (strlen(uuid_str) != (AST_UUID_STR_LEN - 1)) { + ast_log(LOG_ERROR, "AEAP message %s failed to generate UUID for message '%s'", + message->type->type_name, ast_aeap_message_name(message)); + return NULL; + } + + return ast_aeap_message_id_set(message, uuid_str) ? NULL : ast_aeap_message_id(message); +} + +const char *ast_aeap_message_name(const struct ast_aeap_message *message) +{ + const char *name = NULL; + + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + if (message->type->name) { + name = message->type->name(message); + } + + return name ? name : ""; +} + +int ast_aeap_message_is_named(const struct ast_aeap_message *message, const char *name) +{ + return name ? !strcasecmp(ast_aeap_message_name(message), name) : 0; +} + +void *ast_aeap_message_data(struct ast_aeap_message *message) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->data ? message->type->data(message) : NULL; +} + +int ast_aeap_message_is_request(const struct ast_aeap_message *message) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->is_request ? message->type->is_request(message) : 0; +} + +int ast_aeap_message_is_response(const struct ast_aeap_message *message) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->is_response ? message->type->is_response(message) : 0; +} + +const char *ast_aeap_message_error_msg(const struct ast_aeap_message *message) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->error_msg ? message->type->error_msg(message) : NULL; +} + +int ast_aeap_message_error_msg_set(struct ast_aeap_message *message, const char *error_msg) +{ + ast_assert(message != NULL); + ast_assert(message->type != NULL); + + return message->type->error_msg_set ? message->type->error_msg_set(message, error_msg) : 0; +} diff --git a/res/res_aeap/message_json.c b/res/res_aeap/message_json.c new file mode 100644 index 0000000000000000000000000000000000000000..f5cfe9d46134ca268ec01a9cace756c087d461f9 --- /dev/null +++ b/res/res_aeap/message_json.c @@ -0,0 +1,191 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/json.h" + +#include "asterisk/res_aeap_message.h" + +#define JSON_MSG(lvalue, rvalue) struct message_json *lvalue = \ + ((struct message_json *)rvalue) + +/*! + * \brief Asterisk external application JSON message type + */ +struct message_json { + /*! The base message type (must be first) */ + struct ast_aeap_message base; + /*! Underlying JSON data structure */ + struct ast_json *json; +}; + +static int message_json_construct1(struct ast_aeap_message *self, const void *params) +{ + JSON_MSG(msg, self); + + msg->json = ast_json_ref((struct ast_json *)params) ?: ast_json_object_create(); + + return msg->json ? 0 : -1; +} + +static int message_json_construct2(struct ast_aeap_message *self, const char *msg_type, + const char *name, const char *id, const void *params) +{ + struct ast_json *msg_data; + int res; + + msg_data = ast_json_pack("{s:s,s:s*}", msg_type, name, "id", id); + + if (!msg_data) { + ast_log(LOG_ERROR, "AEAP message json: failed to create data for '%s: %s'", msg_type, name); + return -1; + } + + if (params && ast_json_object_update(msg_data, (struct ast_json *)params)) { + ast_log(LOG_ERROR, "AEAP message json: failed to update data for '%s: %s'", msg_type, name); + ast_json_unref(msg_data); + return -1; + } + + res = message_json_construct1(self, msg_data); + ast_json_unref(msg_data); + return res; +} + +static void message_json_destruct(struct ast_aeap_message *self) +{ + JSON_MSG(msg, self); + + ast_json_unref(msg->json); +} + +static int message_json_deserialize(struct ast_aeap_message *self, const void *buf, intmax_t size) +{ + JSON_MSG(msg, self); + + msg->json = ast_json_load_buf(buf, size, NULL); + + return msg->json ? 0 : -1; +} + +static int message_json_serialize(const struct ast_aeap_message *self, void **buf, intmax_t *size) +{ + const JSON_MSG(msg, self); + + *buf = ast_json_dump_string(msg->json); + if (!*buf) { + *size = 0; + return -1; + } + + *size = strlen(*buf); + + return 0; +} + +static const char *message_json_id(const struct ast_aeap_message *self) +{ + const JSON_MSG(msg, self); + + return ast_json_object_string_get(msg->json, "id"); +} + +static int message_json_id_set(struct ast_aeap_message *self, const char *id) +{ + JSON_MSG(msg, self); + + if (ast_json_object_set(msg->json, "id", ast_json_string_create(id))) { + return -1; + } + + return 0; +} + +static const char *message_json_name(const struct ast_aeap_message *self) +{ + const JSON_MSG(msg, self); + struct ast_json_iter *iter; + + iter = ast_json_object_iter_at(msg->json, "response"); + if (!iter) { + iter = ast_json_object_iter_at(msg->json, "request"); + } + + return iter ? ast_json_string_get(ast_json_object_iter_value(iter)) : ""; +} + +static void *message_json_data(struct ast_aeap_message *self) +{ + JSON_MSG(msg, self); + + return msg->json; +} + +static int message_json_is_request(const struct ast_aeap_message *self) +{ + const JSON_MSG(msg, self); + + return ast_json_object_iter_at(msg->json, "request") != NULL; +} + +static int message_json_is_response(const struct ast_aeap_message *self) +{ + const JSON_MSG(msg, self); + + return ast_json_object_iter_at(msg->json, "response") != NULL; +} + +static const char *message_json_error_msg(const struct ast_aeap_message *self) +{ + const JSON_MSG(msg, self); + + return ast_json_object_string_get(msg->json, "error_msg"); +} + +static int message_json_error_msg_set(struct ast_aeap_message *self, const char *error_msg) +{ + JSON_MSG(msg, self); + + if (ast_json_object_set(msg->json, "error_msg", ast_json_string_create(error_msg))) { + return -1; + } + + return 0; +} + +static const struct ast_aeap_message_type message_type_json = { + .type_size = sizeof(struct message_json), + .type_name = "json", + .serial_type = AST_AEAP_DATA_TYPE_STRING, + .construct1 = message_json_construct1, + .construct2 = message_json_construct2, + .destruct = message_json_destruct, + .deserialize = message_json_deserialize, + .serialize = message_json_serialize, + .id = message_json_id, + .id_set = message_json_id_set, + .name = message_json_name, + .data = message_json_data, + .is_request = message_json_is_request, + .is_response = message_json_is_response, + .error_msg = message_json_error_msg, + .error_msg_set = message_json_error_msg_set, +}; + +const struct ast_aeap_message_type *ast_aeap_message_type_json = &message_type_json; diff --git a/res/res_aeap/transaction.c b/res/res_aeap/transaction.c new file mode 100644 index 0000000000000000000000000000000000000000..3f42cb294bddda551aec4aa4c33ac22faf8dba5c --- /dev/null +++ b/res/res_aeap/transaction.c @@ -0,0 +1,284 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/astobj2.h" +#include "asterisk/sched.h" +#include "asterisk/utils.h" + +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#include "general.h" +#include "logger.h" +#include "transaction.h" + +struct aeap_transaction { + /*! Pointer back to owner object */ + struct ast_aeap *aeap; + /*! The container this transaction is in */ + struct ao2_container *container; + /*! Scheduler ID message timeout */ + int sched_id; + /*! Whether or not the handler has been executed */ + int handled; + /*! Used to sync matching received messages */ + ast_cond_t handled_cond; + /*! The result of this transaction */ + int result; + /*! The timeout data */ + struct ast_aeap_tsx_params params; + /*! The transaction identifier */ + char id[0]; +}; + +/*! \brief Number of transaction buckets */ +#define AEAP_TRANSACTION_BUCKETS 11 + +AO2_STRING_FIELD_HASH_FN(aeap_transaction, id); +AO2_STRING_FIELD_CMP_FN(aeap_transaction, id); + +int aeap_transaction_cancel_timer(struct aeap_transaction *tsx) +{ + if (tsx && tsx->sched_id != -1) { + AST_SCHED_DEL_UNREF(aeap_sched_context(), tsx->sched_id, ao2_ref(tsx, -1)); + return tsx->sched_id != -1; + } + + return 0; +} + +void aeap_transaction_params_cleanup(struct ast_aeap_tsx_params *params) +{ + ao2_cleanup(params->msg); + + if (params->obj_cleanup) { + params->obj_cleanup(params->obj); + } +} + +static void transaction_destructor(void *obj) +{ + struct aeap_transaction *tsx = obj; + + /* Ensure timer is canceled */ + aeap_transaction_cancel_timer(tsx); + + aeap_transaction_params_cleanup(&tsx->params); + + ast_cond_destroy(&tsx->handled_cond); +} + +static struct aeap_transaction *transaction_create(const char *id, + struct ast_aeap_tsx_params *params, struct ast_aeap *aeap) +{ + struct aeap_transaction *tsx; + + if (!id) { + aeap_error(aeap, "transaction", "missing transaction id"); + aeap_transaction_params_cleanup(params); + return NULL; + } + + tsx = ao2_alloc(sizeof(*tsx) + strlen(id) + 1, transaction_destructor); + if (!tsx) { + aeap_error(aeap, "transaction", "unable to create for '%s'", id); + aeap_transaction_params_cleanup(params); + return NULL; + } + + strcpy(tsx->id, id); /* safe */ + tsx->sched_id = -1; + + ast_cond_init(&tsx->handled_cond, NULL); + + /* + * Currently, transactions, and their lifetimes are fully managed by the given 'aeap' + * object, so do not bump its reference here as we want the 'aeap' object to stop + * transactions and not transactions potentially stopping the 'aeap' object. + */ + tsx->aeap = aeap; + tsx->params = *params; + + return tsx; +} + +static void transaction_end(struct aeap_transaction *tsx, int timed_out, int result) +{ + if (!tsx) { + return; + } + + ao2_lock(tsx); + + tsx->result = result; + + if (tsx->container) { + ao2_unlink(tsx->container, tsx); + tsx->container = NULL; + } + + if (!timed_out) { + aeap_transaction_cancel_timer(tsx); + } else if (tsx->sched_id != -1) { + tsx->sched_id = -1; + } + + if (!tsx->handled) { + if (timed_out) { + if (tsx->params.on_timeout) { + tsx->params.on_timeout(tsx->aeap, tsx->params.msg, tsx->params.obj); + } else { + aeap_error(tsx->aeap, "transaction", "message '%s' timed out", + ast_aeap_message_name(tsx->params.msg)); + } + } + + tsx->handled = 1; + ast_cond_signal(&tsx->handled_cond); + } + + ao2_unlock(tsx); + + ao2_ref(tsx, -1); +} + +static int transaction_raise_timeout(const void *data) +{ + /* Ref added added at timer creation removed in end call */ + transaction_end((struct aeap_transaction *)data, 1, -1); + + return 0; +} + +static int transaction_sched_timer(struct aeap_transaction *tsx) +{ + if (tsx->params.timeout <= 0 || tsx->sched_id != -1) { + return 0; + } + + tsx->sched_id = ast_sched_add(aeap_sched_context(), tsx->params.timeout, + transaction_raise_timeout, ao2_bump(tsx)); + if (tsx->sched_id == -1) { + aeap_error(tsx->aeap, "transaction", "unable to schedule timeout for '%s'", tsx->id); + ao2_ref(tsx, -1); + return -1; + } + + return 0; +} + +static void transaction_wait(struct aeap_transaction *tsx) +{ + ao2_lock(tsx); + + while (!tsx->handled) { + ast_cond_wait(&tsx->handled_cond, ao2_object_get_lockaddr(tsx)); + } + + ao2_unlock(tsx); +} + +int aeap_transaction_start(struct aeap_transaction *tsx) +{ + if (transaction_sched_timer(tsx)) { + return -1; + } + + if (tsx->params.wait) { + /* Wait until transaction completes, or times out */ + transaction_wait(tsx); + } + + return 0; +} + +struct aeap_transaction *aeap_transaction_get(struct ao2_container *transactions, const char *id) +{ + return ao2_find(transactions, id, OBJ_SEARCH_KEY); +} + +void aeap_transaction_end(struct aeap_transaction *tsx, int result) +{ + transaction_end(tsx, 0, result); +} + +int aeap_transaction_result(struct aeap_transaction *tsx) +{ + return tsx->result; +} + +void *aeap_transaction_user_obj(struct aeap_transaction *tsx) +{ + return tsx->params.obj; +} + +struct aeap_transaction *aeap_transaction_create_and_add(struct ao2_container *transactions, + const char *id, struct ast_aeap_tsx_params *params, struct ast_aeap *aeap) +{ + struct aeap_transaction *tsx; + + tsx = transaction_create(id, params, aeap); + if (!tsx) { + return NULL; + } + + if (!ao2_link(transactions, tsx)) { + aeap_error(tsx->aeap, "transaction", "unable to add '%s' to container", id); + ao2_ref(tsx, -1); + return NULL; + } + + /* + * Yes, this creates a circular reference. This reference is removed though + * upon transaction end. It's assumed here that the given transactions container + * takes "ownership", and ultimate responsibility of its contained transactions. + * Thus when the given container needs to be unref'ed/freed it must call + * aeap_transaction_end for each transaction prior to doing so. + */ + /* tsx->container = ao2_bump(transactions); */ + + /* + * The transaction needs to know what container manages it, so it can remove + * itself from the given container under certain conditions (e.g. transaction + * timeout). + * + * It's expected that the given container will out live any contained transaction + * (i.e. the container will not itself be destroyed before ensuring all contained + * transactions are ended, and removed). Thus there is no reason to bump the given + * container's reference here. + */ + tsx->container = transactions; + + return tsx; +} + +struct ao2_container *aeap_transactions_create(void) +{ + struct ao2_container *transactions; + + transactions = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, AEAP_TRANSACTION_BUCKETS, + aeap_transaction_hash_fn, NULL, aeap_transaction_cmp_fn); + if (!transactions) { + ast_log(LOG_ERROR, "AEAP transaction: unable to create container\n"); + return NULL; + } + + return transactions; +} diff --git a/res/res_aeap/transaction.h b/res/res_aeap/transaction.h new file mode 100644 index 0000000000000000000000000000000000000000..973ba1a4dd9c2958f6494a3292f78475af1fbe94 --- /dev/null +++ b/res/res_aeap/transaction.h @@ -0,0 +1,123 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +#ifndef RES_AEAP_TRANSACTION_H +#define RES_AEAP_TRANSACTION_H + +#include "asterisk/res_aeap.h" + +struct ao2_container; +struct ast_aeap_tsx_params; +struct aeap_transaction; + +/*! + * \brief Create an Asterisk external application transactions container + * + * \returns A transaction object, or NULL on error + */ +struct ao2_container *aeap_transactions_create(void); + +/*! + * \brief Create a transaction object, and add it to the given container + * + * \param transactions A transactions container + * \param id An id to use for the transaction + * \param params Transaction parameters + * \param aeap The aeap object that "owns" this transaction + * + * \returns 0 if successfully create and added, -1 on error + */ +struct aeap_transaction *aeap_transaction_create_and_add(struct ao2_container *transactions, + const char *id, struct ast_aeap_tsx_params *params, struct ast_aeap *aeap); + +/*! + * \brief Clean up parameter references, and possibly call optional user object cleanup + * + * \param params Transaction parameters + */ +void aeap_transaction_params_cleanup(struct ast_aeap_tsx_params *params); + +/*! + * \brief Retrieve a transaction for the id from the container + * + * \param transactions A transactions container + * \param id A transaction id + * + * \returns an AEAP transaction object, NULL if no transaction is found + */ +struct aeap_transaction *aeap_transaction_get(struct ao2_container *transactions, + const char *id); + +/*! + * \brief Start the transaction + * + * \param tsx The transaction to initiate + * + * \returns 0 if successfully raised, and handled. Otherwise non zero. + */ +int aeap_transaction_start(struct aeap_transaction *tsx); + +/*! + * \brief End a transaction, and remove it from the given container + * + * The "result" parameter is a value representing the state (success/failure, + * perhaps even something else) of transactional processing upon ending. + * + * \param tsx A transaction to end + * \param result A result to give to the transaction + */ +void aeap_transaction_end(struct aeap_transaction *tsx, int result); + +/*! + * \brief Get a transaction's result + * + * A transaction's result is a value that represents the relative success (0), or + * failure (-1) of a transaction. For example, a timeout is considered a failure + * and will elicit a -1. + * + * This value though is also dependent upon the result of the message handler + * associated with the transaction. Meaning if an associated message is handled, + * then its result is stored as the transaction result and returned here. + * + * \param tsx A transaction object + * + * \returns The transaction result + */ +int aeap_transaction_result(struct aeap_transaction *tsx); + +/*! + * \brief Cancel the transaction timer + * + * Stops the transaction timer, but does not end/stop the transaction itself + * + * \param transaction A transaction to cancel the timer on + * + * \returns 0 if canceled, non zero otherwise + */ +int aeap_transaction_cancel_timer(struct aeap_transaction *tsx); + +/*! + * \brief Retrieve the user object associated with the transaction + * + * \param transaction A transaction object + * + * \returns A user object, or NULL if non associated + */ +void *aeap_transaction_user_obj(struct aeap_transaction *tsx); + +#endif /* RES_AEAP_TRANSACTION_H */ diff --git a/res/res_aeap/transport.c b/res/res_aeap/transport.c new file mode 100644 index 0000000000000000000000000000000000000000..c032e10ee37c3a0f59de5d19d1a2ffebe81dac6d --- /dev/null +++ b/res/res_aeap/transport.c @@ -0,0 +1,156 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/utils.h" + +#include "logger.h" +#include "transport.h" +#include "transport_websocket.h" + +struct aeap_transport *aeap_transport_create(const char *type) +{ + struct aeap_transport *transport = NULL; + + if (!strncasecmp(type, "ws", 2)) { + transport = (struct aeap_transport *)aeap_transport_websocket_create(); + } + + if (!transport) { + ast_log(LOG_ERROR, "AEAP transport: failed to create for type '%s'\n", type); + return NULL; + } + + ast_mutex_init(&transport->read_lock); + ast_mutex_init(&transport->write_lock); + + transport->connected = 0; + + return transport; +} + +int aeap_transport_connect(struct aeap_transport *transport, const char *url, + const char *protocol, int timeout) +{ + int res; + + SCOPED_MUTEX(rlock, &transport->read_lock); + SCOPED_MUTEX(wlock, &transport->write_lock); + + if (aeap_transport_is_connected(transport)) { + return 0; + } + + res = transport->vtable->connect(transport, url, protocol, timeout); + if (!res) { + transport->connected = 1; + } + + return res; +} + +struct aeap_transport *aeap_transport_create_and_connect(const char *type, + const char *url, const char *protocol, int timeout) +{ + struct aeap_transport *transport = aeap_transport_create(type); + + if (!transport) { + return NULL; + } + + if (aeap_transport_connect(transport, url, protocol, timeout)) { + aeap_transport_destroy(transport); + return NULL; + } + + return transport; +} + +int aeap_transport_is_connected(struct aeap_transport *transport) +{ + /* + * Avoid using a lock to 'read' the 'connected' variable in order to + * keep things slightly more efficient. + */ + return ast_atomic_fetch_add(&transport->connected, 0, __ATOMIC_RELAXED); +} + +int aeap_transport_disconnect(struct aeap_transport *transport) +{ + int res; + + SCOPED_MUTEX(rlock, &transport->read_lock); + SCOPED_MUTEX(wlock, &transport->write_lock); + + if (!aeap_transport_is_connected(transport)) { + return 0; + } + + res = transport->vtable->disconnect(transport); + + /* + * Even though the transport is locked here use atomics to set the value of + * 'connected' since it's possible the variable is being 'read' by another + * thread via the 'is_connected' call. + */ + ast_atomic_fetch_sub(&transport->connected, 1, __ATOMIC_RELAXED); + + return res; +} + +void aeap_transport_destroy(struct aeap_transport *transport) +{ + if (!transport) { + return; + } + + /* Ensure an orderly disconnect occurs before final destruction */ + aeap_transport_disconnect(transport); + + transport->vtable->destroy(transport); + + ast_mutex_destroy(&transport->read_lock); + ast_mutex_destroy(&transport->write_lock); + + ast_free(transport); +} + +intmax_t aeap_transport_read(struct aeap_transport *transport, void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE *rtype) +{ + SCOPED_MUTEX(lock, &transport->read_lock); + + if (!aeap_transport_is_connected(transport)) { + return 0; + } + + return transport->vtable->read(transport, buf, size, rtype); +} + +intmax_t aeap_transport_write(struct aeap_transport *transport, const void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE wtype) +{ + SCOPED_MUTEX(lock, &transport->write_lock); + + if (!aeap_transport_is_connected(transport)) { + return 0; + } + + return transport->vtable->write(transport, buf, size, wtype); +} diff --git a/res/res_aeap/transport.h b/res/res_aeap/transport.h new file mode 100644 index 0000000000000000000000000000000000000000..622247e5ad1a2467772278890e85693aafdf5188 --- /dev/null +++ b/res/res_aeap/transport.h @@ -0,0 +1,209 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +#ifndef RES_AEAP_TRANSPORT_H +#define RES_AEAP_TRANSPORT_H + +#include <stdint.h> + +#include "asterisk/res_aeap.h" + +struct aeap_transport; + +/*! + * \brief Asterisk external application transport virtual table + * + * Callbacks to be implemented by "derived" transports + */ +struct aeap_transport_vtable { + /*! + * \brief Connect a transport + * + * \param self The transport object + * \param url The URL to connect to + * \param protocol The connection protocol to use if applicable + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns 0 on success, or -1 on error + */ + int (*connect)(struct aeap_transport *self, const char *url, const char *protocol, int timeout); + + /*! + * \brief Disconnect a transport + * + * \param self The transport object + * + * \returns 0 on success, or -1 on error + */ + int (*disconnect)(struct aeap_transport *self); + + /*! + * \brief Destroy a transport + * + * \param self The transport object + */ + void (*destroy)(struct aeap_transport *self); + + /*! + * \brief Read data from a transport + * + * \param self The transport object + * \param buf The buffer data is read read into + * \param size The size of the given data buffer + * \param rtype [out] The type of data read + * + * \returns Total number of bytes read, or less than zero on error + */ + intmax_t (*read)(struct aeap_transport *self, void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE *rtype); + + /*! + * \brief Write data to a transport + * + * \param self The transport object + * \param buf The data to write + * \param size The size of data to write + * \param wtype The type of data to write + * + * \returns Total number of bytes written, or less than zero on error + */ + intmax_t (*write)(struct aeap_transport *self, const void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE wtype); +}; + +/*! + * \brief Asterisk external application transport structure to be + * "derived" by specific transport implementation types + * + * Transports are assumed to support simultaneous reading and writing, + * thus separate read and write locks. A transport type not supporting + * such can simply apply the opposing lock during a read or write, i.e. + * lock the write lock during a read and vice versa. + */ +struct aeap_transport { + /*! Transport virtual table */ + struct aeap_transport_vtable *vtable; + /*! Whether or not the transport is connected */ + unsigned int connected; + /*! Lock used when reading */ + ast_mutex_t read_lock; + /*! Lock used when writing */ + ast_mutex_t write_lock; +}; + +/*! + * \brief Create an Asterisk external application transport + * + * \param type The type of transport to create + * + * \returns An Asterisk external application transport, or NULL on error + */ +struct aeap_transport *aeap_transport_create(const char *type); + +/*! + * \brief Connect a transport + * + * \param transport The transport to connect + * \param url The URL to connect to + * \param protocol The connection protocol to use if applicable + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns 0 on success, or -1 on error + */ +int aeap_transport_connect(struct aeap_transport *transport, const char *url, + const char *protocol, int timeout); + +/*! + * \brief Create an Asterisk external application transport, and connect it + * + * \param type The type of transport to create + * \param url The URL to connect to + * \param protocol The connection protocol to use if applicable + * \param timeout How long (in milliseconds) to attempt to connect (-1 equals infinite) + * + * \returns An Asterisk external application transport, or NULL on error + */ +struct aeap_transport *aeap_transport_create_and_connect(const char* type, + const char *url, const char *protocol, int timeout); + +/*! + * \brief Disconnect a transport + * + * \note Locks both the transport's read and write locks before calling transport + * instance's disconnect, and unlocks both before returning. + * + * \param transport The transport to disconnect + * + * \returns 0 on success, or -1 on error + */ +int aeap_transport_disconnect(struct aeap_transport *transport); + +/*! + * \brief Whether or not the transport is in a connected state + * + * \param transport The transport object + * + * \returns True if connected, false otherwise + */ +int aeap_transport_is_connected(struct aeap_transport *transport); + +/*! + * \brief Destroy a transport + * + * \param transport The transport to destroy + * + * \returns 0 on success, or -1 on error + */ +void aeap_transport_destroy(struct aeap_transport *transport); + +/*! + * \brief Read data from the transport + * + * This is a blocking read, and will not return until the transport + * implementation returns. + * + * \note Locks transport's read lock before calling transport instance's + * read, and unlocks it before returning. + * + * \param transport The transport to read from + * \param buf The buffer data is read into + * \param size The size of data given data buffer + * \param rtype [out] The type of data read + * + * \returns Total number of bytes read, or less than zero on error + */ +intmax_t aeap_transport_read(struct aeap_transport *transport, void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE *rtype); + +/*! + * \brief Write data to the transport + * + * \note Locks transport's write lock before calling transport instance's + * write, and unlocks it before returning. + * + * \param transport The transport to write to + * \param buf The data to write + * \param size The size of data to write + * \param wtype The type of data to write + * + * \returns Total number of bytes written, or less than zero on error + */ +intmax_t aeap_transport_write(struct aeap_transport *transport, const void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE wtype); + +#endif /* RES_AEAP_TRANSPORT_H */ diff --git a/res/res_aeap/transport_websocket.c b/res/res_aeap/transport_websocket.c new file mode 100644 index 0000000000000000000000000000000000000000..5f1a4066073ba87f674166cc9780f1764a7a741d --- /dev/null +++ b/res/res_aeap/transport_websocket.c @@ -0,0 +1,249 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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 "asterisk/http_websocket.h" +#include "asterisk/utils.h" + +#include "logger.h" +#include "transport.h" +#include "transport_websocket.h" + +#define log_error(obj, fmt, ...) aeap_error(obj, "websocket", fmt, ##__VA_ARGS__) + +struct aeap_transport_websocket { + /*! Derive from base transport (must be first attribute) */ + struct aeap_transport base; + /*! The underlying websocket */ + struct ast_websocket *ws; +}; + +static int websocket_connect(struct aeap_transport *self, const char *url, + const char *protocol, int timeout) +{ + struct aeap_transport_websocket *transport = (struct aeap_transport_websocket *)self; + enum ast_websocket_result ws_result; + struct ast_websocket_client_options ws_options = { + .uri = url, + .protocols = protocol, + .timeout = timeout, + .tls_cfg = NULL, + }; + + transport->ws = ast_websocket_client_create_with_options(&ws_options, &ws_result); + if (ws_result != WS_OK) { + log_error(self, "connect failure (%d)", (int)ws_result); + return -1; + } + + return 0; +} + +static int websocket_disconnect(struct aeap_transport *self) +{ + struct aeap_transport_websocket *transport = (struct aeap_transport_websocket *)self; + + if (transport->ws) { + ast_websocket_unref(transport->ws); + transport->ws = NULL; + } + + return 0; +} + +static void websocket_destroy(struct aeap_transport *self) +{ + /* + * Disconnect takes care of cleaning up the websocket. Note, disconnect + * was called by the base/dispatch interface prior to calling this + * function so nothing to do here. + */ +} + +static intmax_t websocket_read(struct aeap_transport *self, void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE *rtype) +{ + struct aeap_transport_websocket *transport = (struct aeap_transport_websocket *)self; + + char *payload; + uint64_t bytes_read = 0; + uint64_t total_bytes_read = 0; + enum ast_websocket_opcode opcode; + int fragmented = 0; + + *rtype = AST_AEAP_DATA_TYPE_NONE; + + if (ast_websocket_fd(transport->ws) < 0) { + log_error(self, "unavailable for reading"); + /* Ensure this transport is in a disconnected state */ + aeap_transport_disconnect(self); + return -1; + } + + /* + * This function is called with the read_lock locked. However, the lock needs to be + * unlocked while waiting for input otherwise a deadlock can occur during disconnect + * (disconnect attempts to grab the lock but can't because read holds it here). So + * unlock it prior to waiting. + */ + ast_mutex_unlock(&transport->base.read_lock); + if (ast_websocket_wait_for_input(transport->ws, -1) <= 0) { + ast_mutex_lock(&transport->base.read_lock); + log_error(self, "poll failure: %s", strerror(errno)); + /* Ensure this transport is in a disconnected state */ + aeap_transport_disconnect(self); + return -1; + } + ast_mutex_lock(&transport->base.read_lock); + + if (!transport->ws) { + /* + * It's possible the transport was told to disconnect while waiting for input. + * If so then the websocket will be NULL, so we don't want to continue. + */ + return 0; + } + + do { + if (ast_websocket_read(transport->ws, &payload, &bytes_read, &opcode, + &fragmented) != 0) { + log_error(self, "read failure (%d): %s", opcode, strerror(errno)); + return -1; + } + + if (!bytes_read) { + continue; + } + + if (total_bytes_read + bytes_read > size) { + log_error(self, "attempted to read too many bytes into (%jd) sized buffer", size); + return -1; + } + + memcpy(buf + total_bytes_read, payload, bytes_read); + total_bytes_read += bytes_read; + + } while (opcode == AST_WEBSOCKET_OPCODE_CONTINUATION); + + switch (opcode) { + case AST_WEBSOCKET_OPCODE_CLOSE: + log_error(self, "closed"); + return -1; + case AST_WEBSOCKET_OPCODE_BINARY: + *rtype = AST_AEAP_DATA_TYPE_BINARY; + break; + case AST_WEBSOCKET_OPCODE_TEXT: + *rtype = AST_AEAP_DATA_TYPE_STRING; + + /* Append terminator, but check for overflow first */ + if (total_bytes_read == size) { + log_error(self, "unable to write string terminator"); + return -1; + } + + *((char *)(buf + total_bytes_read)) = '\0'; + break; + default: + /* Ignore all other message types */ + return 0; + } + + return total_bytes_read; +} + +static intmax_t websocket_write(struct aeap_transport *self, const void *buf, intmax_t size, + enum AST_AEAP_DATA_TYPE wtype) +{ + struct aeap_transport_websocket *transport = (struct aeap_transport_websocket *)self; + intmax_t res = 0; + + switch (wtype) { + case AST_AEAP_DATA_TYPE_BINARY: + res = ast_websocket_write(transport->ws, AST_WEBSOCKET_OPCODE_BINARY, + (char *)buf, size); + break; + case AST_AEAP_DATA_TYPE_STRING: + res = ast_websocket_write(transport->ws, AST_WEBSOCKET_OPCODE_TEXT, + (char *)buf, size); + break; + default: + break; + } + + if (res < 0) { + log_error(self, "problem writing to websocket (closed)"); + + /* + * If the underlying socket is closed then ensure the + * transport is in a disconnected state as well. + */ + aeap_transport_disconnect(self); + + return res; + } + + return size; +} + +static struct aeap_transport_vtable *transport_websocket_vtable(void) +{ + static struct aeap_transport_vtable websocket_vtable = { + .connect = websocket_connect, + .disconnect = websocket_disconnect, + .destroy = websocket_destroy, + .read = websocket_read, + .write = websocket_write, + }; + + return &websocket_vtable; +} + +/*! + * \brief Initialize a transport websocket object, and set its virtual table + * + * \param transport The transport to initialize + * + * \returns 0 on success, -1 on error + */ +static int transport_websocket_init(struct aeap_transport_websocket *transport) +{ + transport->ws = NULL; + + ((struct aeap_transport *)transport)->vtable = transport_websocket_vtable(); + + return 0; +} + +struct aeap_transport_websocket *aeap_transport_websocket_create(void) +{ + struct aeap_transport_websocket *transport; + + transport = ast_calloc(1, sizeof(*transport)); + if (!transport) { + ast_log(LOG_ERROR, "AEAP websocket: unable to create transport websocket"); + return NULL; + } + + if (transport_websocket_init(transport)) { + ast_free(transport); + return NULL; + } + + return transport; +} diff --git a/res/res_aeap/transport_websocket.h b/res/res_aeap/transport_websocket.h new file mode 100644 index 0000000000000000000000000000000000000000..d72657e22e364aa167ef8af00f14a9bd53ad337e --- /dev/null +++ b/res/res_aeap/transport_websocket.h @@ -0,0 +1,34 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +#ifndef RES_AEAP_TRANSPORT_WEBSOCKET_H +#define RES_AEAP_TRANSPORT_WEBSOCKET_H + +/*! + * \brief Asterisk external application protocol websocket transport + */ +struct aeap_transport_websocket; + +/*! + * \brief Creates (heap allocated), and initializes a transport websocket + * + * \returns A transport websocket object, or NULL on error + */ +struct aeap_transport_websocket *aeap_transport_websocket_create(void); + +#endif /* RES_AEAP_TRANSPORT_WEBSOCKET_H */ diff --git a/res/res_speech.c b/res/res_speech.c index d425fde181abbf802a1dbfa4dd0a9fa8a48a9443..2438e7953cf44f4c75b01b2b1dea924c425a2176 100644 --- a/res/res_speech.c +++ b/res/res_speech.c @@ -42,7 +42,7 @@ static AST_RWLIST_HEAD_STATIC(engines, ast_speech_engine); static struct ast_speech_engine *default_engine = NULL; /*! \brief Find a speech recognition engine of specified name, if NULL then use the default one */ -static struct ast_speech_engine *find_engine(const char *engine_name) +struct ast_speech_engine *ast_speech_find_engine(const char *engine_name) { struct ast_speech_engine *engine = NULL; @@ -185,7 +185,7 @@ struct ast_speech *ast_speech_new(const char *engine_name, const struct ast_form RAII_VAR(struct ast_format *, best, NULL, ao2_cleanup); /* Try to find the speech recognition engine that was requested */ - if (!(engine = find_engine(engine_name))) + if (!(engine = ast_speech_find_engine(engine_name))) return NULL; joint = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT); @@ -313,7 +313,7 @@ int ast_speech_register(struct ast_speech_engine *engine) } /* If an engine is already loaded with this name, error out */ - if (find_engine(engine->name)) { + if (ast_speech_find_engine(engine->name)) { ast_log(LOG_WARNING, "Speech recognition engine '%s' already exists.\n", engine->name); return -1; } @@ -366,6 +366,36 @@ struct ast_speech_engine *ast_speech_unregister2(const char *engine_name) return engine; } +void ast_speech_unregister_engines( + int (*should_unregister)(const struct ast_speech_engine *engine, void *data), void *data, + void (*on_unregistered)(void *obj)) +{ + struct ast_speech_engine *engine = NULL; + + if (!should_unregister) { + return; + } + + AST_RWLIST_WRLOCK(&engines); + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&engines, engine, list) { + if (should_unregister(engine, data)) { + /* We have our engine... removed it */ + AST_RWLIST_REMOVE_CURRENT(list); + /* If this was the default engine, we need to pick a new one */ + if (engine == default_engine) { + default_engine = AST_RWLIST_FIRST(&engines); + } + ast_verb(2, "Unregistered speech recognition engine '%s'\n", engine->name); + /* All went well */ + if (on_unregistered) { + on_unregistered(engine); + } + } + } + AST_RWLIST_TRAVERSE_SAFE_END; + AST_RWLIST_UNLOCK(&engines); +} + static int unload_module(void) { /* We can not be unloaded */ diff --git a/res/res_speech_aeap.c b/res/res_speech_aeap.c new file mode 100644 index 0000000000000000000000000000000000000000..d81d6e5dcba34c9fab26a5b803e3cb7f3938e905 --- /dev/null +++ b/res/res_speech_aeap.c @@ -0,0 +1,731 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*! \file + * + * \brief Asterisk External Application Speech Engine + * + */ + +/*** MODULEINFO + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include "asterisk/astobj2.h" +#include "asterisk/config.h" +#include "asterisk/format.h" +#include "asterisk/format_cap.h" +#include "asterisk/json.h" +#include "asterisk/module.h" +#include "asterisk/speech.h" +#include "asterisk/sorcery.h" + +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#define SPEECH_AEAP_VERSION "0.1.0" +#define SPEECH_PROTOCOL "speech_to_text" + +#define CONNECTION_TIMEOUT 2000 + +#define log_error(obj, fmt, ...) \ + ast_log(LOG_ERROR, "AEAP speech (%p): " fmt "\n", obj, ##__VA_ARGS__) + +static struct ast_json *custom_fields_to_params(const struct ast_variable *variables) +{ + const struct ast_variable *i; + struct ast_json *obj; + + if (!variables) { + return NULL; + } + + obj = ast_json_object_create(); + if (!obj) { + return NULL; + } + + for (i = variables; i; i = i->next) { + if (i->name[0] == '@' && i->name[1]) { + ast_json_object_set(obj, i->name + 1, ast_json_string_create(i->value)); + } + } + + return obj; +} + +/*! + * \internal + * \brief Create, and send a request to the external application + * + * Create, then sends a request to an Asterisk external application, and then blocks + * until a response is received or a time out occurs. Since this method waits until + * receiving a response the returned result is guaranteed to be pass/fail based upon + * a response handler's result. + * + * \param name The name of the request to send + * \param json The core json request data + * \param data Optional user data to associate with request/response + * + * \returns 0 on success, -1 on error + */ +static int speech_aeap_send_request(struct ast_aeap *aeap, const char *name, + struct ast_json *json, void *obj) +{ + /* + * Wait for a response. Also since we're blocking, + * data is expected to be on the stack so no cleanup required. + */ + struct ast_aeap_tsx_params tsx_params = { + .timeout = 1000, + .wait = 1, + .obj = obj, + }; + + /* "steals" the json ref */ + tsx_params.msg = ast_aeap_message_create_request( + ast_aeap_message_type_json, name, NULL, json); + if (!tsx_params.msg) { + return -1; + } + + /* Send "steals" the json msg ref */ + return ast_aeap_send_msg_tsx(aeap, &tsx_params); +} + +/*! + * \internal + * \brief Create, and send a "get" request to an external application + * + * Basic structure of the JSON message to send: + * + * { param: [<param>, ...] } + * + * \param speech The speech engine + * \param param The name of the parameter to retrieve + * \param data User data passed to the response handler + * + * \returns 0 on success, -1 on error + */ +static int speech_aeap_get(struct ast_speech *speech, const char *param, void *data) +{ + if (!param) { + return -1; + } + + /* send_request handles json ref */ + return speech_aeap_send_request(speech->data, + "get", ast_json_pack("{s:[s]}", "params", param), data); +} + +struct speech_param { + const char *name; + const char *value; +}; + +/*! + * \internal + * \brief Create, and send a "set" request to an external application + * + * Basic structure of the JSON message to send: + * + * { params: { <name> : <value> } } + * + * \param speech The speech engine + * \param name The name of the parameter to set + * \param value The value of the parameter to set + * + * \returns 0 on success, -1 on error + */ +static int speech_aeap_set(struct ast_speech *speech, const char *name, const char *value) +{ + if (!name) { + return -1; + } + + /* send_request handles json ref */ + return speech_aeap_send_request(speech->data, + "set", ast_json_pack("{s:{s:s}}", "params", name, value), NULL); +} + +static int handle_response_set(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + return 0; +} + +struct speech_setting { + const char *param; + size_t len; + char *buf; +}; + +static int handle_setting(struct ast_aeap *aeap, struct ast_json_iter *iter, + struct speech_setting *setting) +{ + const char *value; + + if (strcmp(ast_json_object_iter_key(iter), setting->param)) { + log_error(aeap, "Unable to 'get' speech setting for '%s'", setting->param); + return -1; + } + + value = ast_json_string_get(ast_json_object_iter_value(iter)); + if (!value) { + log_error(aeap, "No value for speech setting '%s'", setting->param); + return -1; + } + + ast_copy_string(setting->buf, value, setting->len); + return 0; +} + +static int handle_results(struct ast_aeap *aeap, struct ast_json_iter *iter, + struct ast_speech_result **speech_results) +{ + struct ast_speech_result *result = NULL; + struct ast_json *json_results; + struct ast_json *json_result; + size_t i; + + json_results = ast_json_object_iter_value(iter); + if (!json_results || !speech_results) { + log_error(aeap, "Unable to 'get' speech results"); + return -1; + } + + for (i = 0; i < ast_json_array_size(json_results); ++i) { + if (!(result = ast_calloc(1, sizeof(*result)))) { + continue; + } + + json_result = ast_json_array_get(json_results, i); + + result->text = ast_strdup(ast_json_object_string_get(json_result, "text")); + result->score = ast_json_object_integer_get(json_result, "score"); + result->grammar = ast_strdup(ast_json_object_string_get(json_result, "grammar")); + result->nbest_num = ast_json_object_integer_get(json_result, "best"); + if (*speech_results) { + AST_LIST_NEXT(result, list) = *speech_results; + *speech_results = result; + } else { + *speech_results = result; + } + } + + return 0; +} + +/*! + * \internal + * \brief Handle a "get" response from an external application + * + * Basic structure of the expected JSON message to received: + * + * { + * response: "get" + * "params" : { <name>: <value> | [ <results> ] } + * } + * + * \param speech The speech engine + * \param param The name of the parameter to retrieve + * \param data User data passed to the response handler + * + * \returns 0 on success, -1 on error + */ +static int handle_response_get(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + struct ast_json_iter *iter; + + iter = ast_json_object_iter(ast_json_object_get(ast_aeap_message_data(message), "params")); + if (!iter) { + log_error(aeap, "no 'get' parameters returned"); + return -1; + } + + if (!strcmp(ast_json_object_iter_key(iter), "results")) { + return handle_results(aeap, iter, data); + } + + return handle_setting(aeap, iter, data); +} + +static int handle_response_setup(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + struct ast_format *format = data; + struct ast_json *json = ast_aeap_message_data(message); + const char *codec_name; + + if (!json) { + log_error(aeap, "no 'setup' object returned"); + return -1; + } + + json = ast_json_object_get(json, "codecs"); + if (!json || ast_json_array_size(json) == 0) { + log_error(aeap, "no 'setup' codecs available"); + return -1; + } + + codec_name = ast_json_object_string_get(ast_json_array_get(json, 0), "name"); + if (!codec_name || strcmp(codec_name, ast_format_get_codec_name(format))) { + log_error(aeap, "setup codec '%s' unsupported", ast_format_get_codec_name(format)); + return -1; + } + + return 0; +} + +static const struct ast_aeap_message_handler response_handlers[] = { + { "setup", handle_response_setup }, + { "get", handle_response_get }, + { "set", handle_response_set }, +}; + +static int handle_request_set(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + struct ast_json_iter *iter; + const char *error_msg = NULL; + + iter = ast_json_object_iter(ast_json_object_get(ast_aeap_message_data(message), "params")); + if (!iter) { + error_msg = "no parameter(s) requested"; + } else if (!strcmp(ast_json_object_iter_key(iter), "results")) { + struct ast_speech *speech = ast_aeap_user_data_object_by_id(aeap, "speech"); + + if (!speech) { + error_msg = "no associated speech object"; + } else if (handle_results(aeap, iter, &speech->results)) { + error_msg = "unable to handle results"; + } else { + ast_speech_change_state(speech, AST_SPEECH_STATE_DONE); + } + } else { + error_msg = "can only set 'results'"; + } + + if (error_msg) { + log_error(aeap, "set - %s", error_msg); + message = ast_aeap_message_create_error(ast_aeap_message_type_json, + ast_aeap_message_name(message), ast_aeap_message_id(message), error_msg); + } else { + message = ast_aeap_message_create_response(ast_aeap_message_type_json, + ast_aeap_message_name(message), ast_aeap_message_id(message), NULL); + } + + ast_aeap_send_msg(aeap, message); + + return 0; +} + +static const struct ast_aeap_message_handler request_handlers[] = { + { "set", handle_request_set }, +}; + +static struct ast_aeap_params speech_aeap_params = { + .response_handlers = response_handlers, + .response_handlers_size = ARRAY_LEN(response_handlers), + .request_handlers = request_handlers, + .request_handlers_size = ARRAY_LEN(request_handlers), +}; + +/*! + * \internal + * \brief Create, and connect to an external application and send initial setup + * + * Basic structure of the JSON message to send: + * + * { + * "request": "setup" + * "codecs": [ + * { + * "name": <name>, + * "attributes": { <name>: <value>, ..., } + * }, + * ..., + * ], + * "params": { <name>: <value>, ..., } + * } + * + * \param speech The speech engine + * \param format The format codec to use + * + * \returns 0 on success, -1 on error + */ +static int speech_aeap_engine_create(struct ast_speech *speech, struct ast_format *format) +{ + struct ast_aeap *aeap; + struct ast_variable *vars; + struct ast_json *json; + + aeap = ast_aeap_create_and_connect_by_id( + speech->engine->name, &speech_aeap_params, CONNECTION_TIMEOUT); + if (!aeap) { + return -1; + } + + speech->data = aeap; + + /* Don't allow unloading of this module while an external application is in use */ + ast_module_ref(ast_module_info->self); + + vars = ast_aeap_custom_fields_get(speech->engine->name); + + /* While the protocol allows sending of codec attributes, for now don't */ + json = ast_json_pack("{s:s,s:[{s:s}],s:o*}", "version", SPEECH_AEAP_VERSION, "codecs", + "name", ast_format_get_codec_name(format), "params", custom_fields_to_params(vars)); + + ast_variables_destroy(vars); + + if (ast_aeap_user_data_register(aeap, "speech", speech, NULL)) { + ast_module_unref(ast_module_info->self); + return -1; + } + + /* send_request handles json ref */ + if (speech_aeap_send_request(speech->data, "setup", json, format)) { + ast_module_unref(ast_module_info->self); + return -1; + } + + /* + * Add a reference to the engine here, so if it happens to get unregistered + * while executing it won't disappear. + */ + ao2_ref(speech->engine, 1); + + return 0; +} + +static int speech_aeap_engine_destroy(struct ast_speech *speech) +{ + ao2_ref(speech->engine, -1); + ao2_cleanup(speech->data); + + ast_module_unref(ast_module_info->self); + + return 0; +} + +static int speech_aeap_engine_write(struct ast_speech *speech, void *data, int len) +{ + return ast_aeap_send_binary(speech->data, data, len); +} + +static int speech_aeap_engine_dtmf(struct ast_speech *speech, const char *dtmf) +{ + return speech_aeap_set(speech, "dtmf", dtmf); +} + +static int speech_aeap_engine_start(struct ast_speech *speech) +{ + ast_speech_change_state(speech, AST_SPEECH_STATE_READY); + + return 0; +} + +static int speech_aeap_engine_change(struct ast_speech *speech, const char *name, const char *value) +{ + return speech_aeap_set(speech, name, value); +} + +static int speech_aeap_engine_get_setting(struct ast_speech *speech, const char *name, + char *buf, size_t len) +{ + struct speech_setting setting = { + .param = name, + .len = len, + .buf = buf, + }; + + return speech_aeap_get(speech, name, &setting); +} + +static int speech_aeap_engine_change_results_type(struct ast_speech *speech, + enum ast_speech_results_type results_type) +{ + return speech_aeap_set(speech, "results_type", + ast_speech_results_type_to_string(results_type)); +} + +static struct ast_speech_result *speech_aeap_engine_get(struct ast_speech *speech) +{ + struct ast_speech_result *results = NULL; + + if (speech->results) { + return speech->results; + } + + if (speech_aeap_get(speech, "results", &results)) { + return NULL; + } + + return results; +} + +static void speech_engine_destroy(void *obj) +{ + struct ast_speech_engine *engine = obj; + + ao2_cleanup(engine->formats); + ast_free(engine->name); +} + +static struct ast_speech_engine *speech_engine_alloc(const char *name) +{ + struct ast_speech_engine *engine; + + engine = ao2_t_alloc_options(sizeof(*engine), speech_engine_destroy, + AO2_ALLOC_OPT_LOCK_NOLOCK, name); + if (!engine) { + ast_log(LOG_ERROR, "AEAP speech: unable create engine '%s'\n", name); + return NULL; + } + + engine->name = ast_strdup(name); + if (!engine->name) { + ao2_ref(engine, -1); + return NULL; + } + + engine->create = speech_aeap_engine_create; + engine->destroy = speech_aeap_engine_destroy; + engine->write = speech_aeap_engine_write; + engine->dtmf = speech_aeap_engine_dtmf; + engine->start = speech_aeap_engine_start; + engine->change = speech_aeap_engine_change; + engine->get_setting = speech_aeap_engine_get_setting; + engine->change_results_type = speech_aeap_engine_change_results_type; + engine->get = speech_aeap_engine_get; + + engine->formats = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT); + + return engine; +} + +static void speech_engine_alloc_and_register(const char *name, const struct ast_format_cap *formats) +{ + struct ast_speech_engine *engine; + + engine = speech_engine_alloc(name); + if (!engine) { + return; + } + + if (formats && ast_format_cap_append_from_cap(engine->formats, + formats, AST_MEDIA_TYPE_AUDIO)) { + ast_log(LOG_WARNING, "AEAP speech: Unable to add engine '%s' formats\n", name); + ao2_ref(engine, -1); + return; + } + + if (ast_speech_register(engine)) { + ast_log(LOG_WARNING, "AEAP speech: Unable to register engine '%s'\n", name); + ao2_ref(engine, -1); + } +} + +#ifdef TEST_FRAMEWORK + +static void speech_engine_alloc_and_register2(const char *name, const char *codec_names) +{ + struct ast_speech_engine *engine; + + engine = speech_engine_alloc(name); + if (!engine) { + return; + } + + if (codec_names && ast_format_cap_update_by_allow_disallow(engine->formats, codec_names, 1)) { + ast_log(LOG_WARNING, "AEAP speech: Unable to add engine '%s' codecs\n", name); + ao2_ref(engine, -1); + return; + } + + if (ast_speech_register(engine)) { + ast_log(LOG_WARNING, "AEAP speech: Unable to register engine '%s'\n", name); + ao2_ref(engine, -1); + } +} + +#endif + +static int unload_engine(void *obj, void *arg, int flags) +{ + if (ast_aeap_client_config_has_protocol(obj, SPEECH_PROTOCOL)) { + ao2_cleanup(ast_speech_unregister2(ast_sorcery_object_get_id(obj))); + } + + return 0; +} + +static int load_engine(void *obj, void *arg, int flags) +{ + const char *id; + const struct ast_format_cap *formats; + const struct ast_speech_engine *engine; + + if (!ast_aeap_client_config_has_protocol(obj, SPEECH_PROTOCOL)) { + return 0; + } + + id = ast_sorcery_object_get_id(obj); + formats = ast_aeap_client_config_codecs(obj); + if (!formats) { + formats = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT); + if (!formats) { + ast_log(LOG_ERROR, "AEAP speech: unable to allocate default engine format for '%s'\n", id); + return 0; + } + } + + engine = ast_speech_find_engine(id); + if (!engine) { + speech_engine_alloc_and_register(id, formats); + return 0; + } + + if (ast_format_cap_identical(formats, engine->formats)) { + /* Same name, same formats then nothing changed */ + return 0; + } + + ao2_ref(ast_speech_unregister2(engine->name), -1); + speech_engine_alloc_and_register(id, formats); + + return 0; +} + +static int matches_engine(void *obj, void *arg, int flags) +{ + const struct ast_speech_engine *engine = arg; + + return strcmp(ast_sorcery_object_get_id(obj), engine->name) ? 0 : CMP_MATCH; +} + +static int should_unregister(const struct ast_speech_engine *engine, void *data) +{ + void *obj; + + if (engine->create != speech_aeap_engine_create) { + /* Only want to potentially unregister AEAP speech engines */ + return 0; + } + +#ifdef TEST_FRAMEWORK + if (!strcmp("_aeap_test_speech_", engine->name)) { + /* Don't remove the test engine */ + return 0; + } +#endif + + obj = ao2_callback(data, 0, matches_engine, (void*)engine); + + if (obj) { + ao2_ref(obj, -1); + return 0; + } + + /* If no match in given container then unregister engine */ + return 1; +} + +static void speech_observer_loaded(const char *object_type) +{ + struct ao2_container *container; + + if (strcmp(object_type, AEAP_CONFIG_CLIENT)) { + return; + } + + container = ast_aeap_client_configs_get(SPEECH_PROTOCOL); + if (!container) { + return; + } + + /* + * An AEAP module reload has occurred. First + * remove all engines that no longer exist. + */ + ast_speech_unregister_engines(should_unregister, container, __ao2_cleanup); + + /* Now add or update engines */ + ao2_callback(container, 0, load_engine, NULL); + ao2_ref(container, -1); +} + +/*! \brief Observer for AEAP reloads */ +static const struct ast_sorcery_observer speech_observer = { + .loaded = speech_observer_loaded, +}; + +static int unload_module(void) +{ + struct ao2_container *container; + +#ifdef TEST_FRAMEWORK + ao2_cleanup(ast_speech_unregister2("_aeap_test_speech_")); +#endif + + ast_sorcery_observer_remove(ast_aeap_sorcery(), AEAP_CONFIG_CLIENT, &speech_observer); + + container = ast_aeap_client_configs_get(SPEECH_PROTOCOL); + if (container) { + ao2_callback(container, 0, unload_engine, NULL); + ao2_ref(container, -1); + } + + return 0; +} + +static int load_module(void) +{ + struct ao2_container *container; + + speech_aeap_params.msg_type = ast_aeap_message_type_json; + + container = ast_aeap_client_configs_get(SPEECH_PROTOCOL); + if (container) { + ao2_callback(container, 0, load_engine, NULL); + ao2_ref(container, -1); + } + + /* + * Add an observer since a named speech server must be created, + * registered, and eventually removed for all AEAP client + * configuration matching the "speech_to_text" protocol. + */ + if (ast_sorcery_observer_add(ast_aeap_sorcery(), AEAP_CONFIG_CLIENT, &speech_observer)) { + return AST_MODULE_LOAD_DECLINE; + } + +#ifdef TEST_FRAMEWORK + speech_engine_alloc_and_register2("_aeap_test_speech_", "ulaw"); +#endif + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Asterisk External Application Speech Engine", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, + .requires = "res_speech,res_aeap", +); diff --git a/tests/test_aeap.c b/tests/test_aeap.c new file mode 100644 index 0000000000000000000000000000000000000000..899da7a75879740a3a0fec079f48256d7842c73c --- /dev/null +++ b/tests/test_aeap.c @@ -0,0 +1,252 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>res_aeap</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/file.h" +#include "asterisk/http_websocket.h" +#include "asterisk/json.h" + +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#define CATEGORY "/res/aeap/" + +#define ADDR "127.0.0.1:8088" +#define AEAP_TRANSPORT_TYPE "ws" +#define AEAP_REMOTE_URL "ws://" ADDR "/ws" +#define AEAP_REMOTE_PROTOCOL "echo" +#define AEAP_MESSAGE_ID "foo" +#define AEAP_CONNECTION_TIMEOUT 2000 + +AST_TEST_DEFINE(create_and_connect) +{ + RAII_VAR(struct ast_aeap *, aeap, NULL, ao2_cleanup); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test creating and connecting to an AEAP application"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (aeap = ast_aeap_create_and_connect(AEAP_TRANSPORT_TYPE, + NULL, AEAP_REMOTE_URL, AEAP_REMOTE_PROTOCOL, AEAP_CONNECTION_TIMEOUT))); + + return AST_TEST_PASS; +} + +static void handle_string(struct ast_aeap *aeap, const char *buf, intmax_t size) +{ + int *passed = ast_aeap_user_data_object_by_id(aeap, AEAP_MESSAGE_ID); + + if (strstr(buf, AEAP_MESSAGE_ID)) { + ++*passed; + } +} + +static void handle_timeout(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + int *passed = ast_aeap_user_data_object_by_id(aeap, AEAP_MESSAGE_ID); + + ++*passed; +} + +AST_TEST_DEFINE(send_msg_handle_string) +{ + int passed = 0; + RAII_VAR(struct ast_aeap *, aeap, NULL, ao2_cleanup); + struct ast_aeap_tsx_params tsx_params = {0}; + struct ast_aeap_params aeap_params = { + .on_string = handle_string, + }; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test an AEAP application string handler"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + tsx_params.timeout = 2000; /* Test will end by timing out */ + tsx_params.on_timeout = handle_timeout; + tsx_params.wait = 1; + + ast_test_validate(test, (aeap = ast_aeap_create_and_connect(AEAP_TRANSPORT_TYPE, + &aeap_params, AEAP_REMOTE_URL, AEAP_REMOTE_PROTOCOL, AEAP_CONNECTION_TIMEOUT))); + + ast_test_validate(test, (!ast_aeap_user_data_register(aeap, AEAP_MESSAGE_ID, &passed, NULL))); + ast_test_validate(test, (tsx_params.msg = ast_aeap_message_create_request( + ast_aeap_message_type_json, "foo", AEAP_MESSAGE_ID, NULL))); + ast_test_validate(test, ast_aeap_send_msg_tsx(aeap, &tsx_params)); /* Returns fail on timeout */ + ast_aeap_user_data_unregister(aeap, AEAP_MESSAGE_ID); + + return passed == 2 ? AST_TEST_PASS : AST_TEST_FAIL; +} + +static int handle_msg(struct ast_aeap *aeap, struct ast_aeap_message *message, void *data) +{ + int *passed = ast_aeap_user_data_object_by_id(aeap, AEAP_MESSAGE_ID); + + *passed = !strcmp(ast_aeap_message_id(message), AEAP_MESSAGE_ID) && + ast_aeap_message_is_named(message, data); + + if (!*passed) { + ast_log(LOG_ERROR, "Name '%s' did not equal '%s' for message '%s'", + ast_aeap_message_name(message), (char *)data, ast_aeap_message_id(message)); + } + + return 0; +} + +static const struct ast_aeap_message_handler handlers[] = { + { "foo", handle_msg }, +}; + +AST_TEST_DEFINE(send_msg_handle_response) +{ + int passed = 0; + RAII_VAR(struct ast_aeap *, aeap, NULL, ao2_cleanup); + char *name = "foo"; + struct ast_aeap_params aeap_params = { + .response_handlers = handlers, + .response_handlers_size = ARRAY_LEN(handlers), + }; + struct ast_aeap_tsx_params tsx_params = {0}; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test an AEAP application response handler"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + aeap_params.msg_type = ast_aeap_message_type_json; + + tsx_params.timeout = 2000; + tsx_params.wait = 1; + tsx_params.obj = name; + + ast_test_validate(test, (aeap = ast_aeap_create_and_connect(AEAP_TRANSPORT_TYPE, + &aeap_params, AEAP_REMOTE_URL, AEAP_REMOTE_PROTOCOL, AEAP_CONNECTION_TIMEOUT))); + ast_test_validate(test, (!ast_aeap_user_data_register(aeap, AEAP_MESSAGE_ID, &passed, NULL))); + ast_test_validate(test, (tsx_params.msg = ast_aeap_message_create_response( + ast_aeap_message_type_json, name, AEAP_MESSAGE_ID, NULL))); + ast_test_validate(test, !ast_aeap_send_msg_tsx(aeap, &tsx_params)); + ast_aeap_user_data_unregister(aeap, AEAP_MESSAGE_ID); + + return passed ? AST_TEST_PASS : AST_TEST_FAIL; +} + +AST_TEST_DEFINE(send_msg_handle_request) +{ + int passed = 0; + RAII_VAR(struct ast_aeap *, aeap, NULL, ao2_cleanup); + char *name = "foo"; + struct ast_aeap_params aeap_params = { + .request_handlers = handlers, + .request_handlers_size = ARRAY_LEN(handlers), + }; + struct ast_aeap_tsx_params tsx_params = {0}; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test an AEAP application request handler"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + aeap_params.msg_type = ast_aeap_message_type_json; + + tsx_params.timeout = 2000; + tsx_params.wait = 1; + tsx_params.obj = name; + + ast_test_validate(test, (aeap = ast_aeap_create_and_connect(AEAP_TRANSPORT_TYPE, + &aeap_params, AEAP_REMOTE_URL, AEAP_REMOTE_PROTOCOL, AEAP_CONNECTION_TIMEOUT))); + ast_test_validate(test, (!ast_aeap_user_data_register(aeap, AEAP_MESSAGE_ID, &passed, NULL))); + ast_test_validate(test, (tsx_params.msg = ast_aeap_message_create_request( + ast_aeap_message_type_json, name, AEAP_MESSAGE_ID, NULL))); + ast_test_validate(test, !ast_aeap_send_msg_tsx(aeap, &tsx_params)); + ast_aeap_user_data_unregister(aeap, AEAP_MESSAGE_ID); + + return passed ? AST_TEST_PASS : AST_TEST_FAIL; +} + +static struct ast_http_server *http_server; + +static int load_module(void) +{ + if (!(http_server = ast_http_test_server_get("aeap transport http server", NULL))) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(create_and_connect); + AST_TEST_REGISTER(send_msg_handle_string); + AST_TEST_REGISTER(send_msg_handle_response); + AST_TEST_REGISTER(send_msg_handle_request); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(send_msg_handle_request); + AST_TEST_UNREGISTER(send_msg_handle_response); + AST_TEST_UNREGISTER(send_msg_handle_string); + AST_TEST_UNREGISTER(create_and_connect); + + ast_http_test_server_discard(http_server); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk External Application Protocol Object Tests", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .requires = "res_aeap", +); diff --git a/tests/test_aeap_speech.c b/tests/test_aeap_speech.c new file mode 100644 index 0000000000000000000000000000000000000000..2e658c1f3991c97ee37aedc45998438b35f97c7d --- /dev/null +++ b/tests/test_aeap_speech.c @@ -0,0 +1,287 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>res_aeap</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/file.h" +#include "asterisk/format_cap.h" +#include "asterisk/http.h" +#include "asterisk/http_websocket.h" +#include "asterisk/json.h" +#include "asterisk/speech.h" + +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#define ADDR "127.0.0.1:8088" + +static int speech_test_server_setup(struct ast_json *req, struct ast_json *resp) +{ + struct ast_json *params; + + if (ast_json_object_set(resp, "codecs", ast_json_ref(ast_json_object_get(req, "codecs")))) { + return -1; + } + + params = ast_json_object_get(req, "params"); /* Optional */ + if (params && ast_json_object_set(resp, "params", ast_json_ref(params))) { + return -1; + } + + return 0; +} + +#define TEST_SPEECH_RESULTS_TEXT "foo" +#define TEST_SPEECH_RESULTS_SCORE 7 +#define TEST_SPEECH_RESULTS_GRAMMAR "bar" +#define TEST_SPEECH_RESULTS_BEST 1 + +static int speech_test_server_get(struct ast_json *req, struct ast_json *resp) +{ + const char *param; + struct ast_json *json = NULL; + + param = ast_json_string_get(ast_json_array_get(ast_json_object_get(req, "params"), 0)); + if (!param) { + return -1; + } + + if (!strcmp(param, "results")) { + json = ast_json_pack("{s:[{s:s,s:i,s:s,s:i}]}", + param, + "text", TEST_SPEECH_RESULTS_TEXT, + "score", TEST_SPEECH_RESULTS_SCORE, + "grammar", TEST_SPEECH_RESULTS_GRAMMAR, + "best", TEST_SPEECH_RESULTS_BEST); + } else { + /* Assume setting */ + json = ast_json_pack("{s:s}", param, "bar"); + } + + if (!json || ast_json_object_set(resp, "params", json)) { + return -1; + } + + return 0; +} + +static int speech_test_server_set(struct ast_json *req, struct ast_json *resp) +{ + if (ast_json_object_set(resp, "params", ast_json_ref(ast_json_object_get(req, "params")))) { + return -1; + } + + return 0; +} + +static int speech_test_server_handle_request(struct ast_websocket *ws, const void *buf, uint64_t size) +{ + struct ast_json *req; + struct ast_json *resp; + const char *name; + char *resp_buf; + int res = 0; + + req = ast_json_load_buf(buf, size, NULL); + if (!req) { + ast_log(LOG_ERROR, "speech test handle request: unable to load json\n"); + return -1; + } + + name = ast_json_object_string_get(req, "request"); + if (!name) { + ast_log(LOG_ERROR, "speech test handle request: no name\n"); + ast_json_unref(req); + return -1; + } + + resp = ast_json_pack("{s:s, s:s}", "response", name, + "id", ast_json_object_string_get(req, "id")); + if (!resp) { + ast_log(LOG_ERROR, "speech test handle request: unable to create response '%s'\n", name); + ast_json_unref(req); + return -1; + } + + if (!strcmp(name, "setup")) { + res = speech_test_server_setup(req, resp); + } else if (!strcmp(name, "get")) { + res = speech_test_server_get(req, resp); + } else if (!strcmp(name, "set")) { + res = speech_test_server_set(req, resp); + } else { + ast_log(LOG_ERROR, "speech test handle request: unsupported request '%s'\n", name); + return -1; + } + + if (res) { + ast_log(LOG_ERROR, "speech test handle request: unable to build response '%s'\n", name); + ast_json_unref(resp); + ast_json_unref(req); + return -1; + } + + resp_buf = ast_json_dump_string(resp); + ast_json_unref(resp); + + if (!resp_buf) { + ast_log(LOG_ERROR, "speech test handle request: unable to dump response '%s'\n", name); + ast_json_unref(req); + return -1; + } + + res = ast_websocket_write_string(ws, resp_buf); + if (res) { + ast_log(LOG_ERROR, "speech test handle request: unable to write response '%s'\n", name); + } + + ast_json_unref(req); + ast_free(resp_buf); + + return res; +} + +static void speech_test_server_cb(struct ast_websocket *ws, struct ast_variable *parameters, + struct ast_variable *headers) +{ + int res; + + if (ast_fd_set_flags(ast_websocket_fd(ws), O_NONBLOCK)) { + ast_websocket_unref(ws); + return; + } + + while ((res = ast_websocket_wait_for_input(ws, -1)) > 0) { + char *payload; + uint64_t payload_len; + enum ast_websocket_opcode opcode; + int fragmented; + + if (ast_websocket_read(ws, &payload, &payload_len, &opcode, &fragmented)) { + ast_log(LOG_ERROR, "speech test: Read failure in server loop\n"); + break; + } + + switch (opcode) { + case AST_WEBSOCKET_OPCODE_CLOSE: + ast_websocket_unref(ws); + return; + case AST_WEBSOCKET_OPCODE_BINARY: + ast_websocket_write(ws, opcode, payload, payload_len); + break; + case AST_WEBSOCKET_OPCODE_TEXT: + ast_debug(3, "payload=%.*s\n", (int)payload_len, payload); + if (speech_test_server_handle_request(ws, payload, payload_len)) { + ast_websocket_unref(ws); + return; + } + break; + default: + break; + } + } + ast_websocket_unref(ws); +} + +AST_TEST_DEFINE(res_speech_aeap_test) +{ + RAII_VAR(struct ast_format_cap *, cap, NULL, ao2_cleanup); + RAII_VAR(struct ast_speech_result *, results, NULL, ast_speech_results_free); + struct ast_speech *speech = NULL; + enum ast_test_result_state res = AST_TEST_PASS; + char buf[8] = ""; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = "/res/aeap/speech/"; + info->summary = "test the speech AEAP interface"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, !ast_websocket_add_protocol("_aeap_test_speech_", speech_test_server_cb)); + + ast_test_validate(test, (cap = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT))); + ast_test_validate(test, !ast_format_cap_update_by_allow_disallow(cap, "ulaw", 1)); + + ast_test_validate_cleanup(test, (speech = ast_speech_new("_aeap_test_speech_", cap)), res, cleanup); + ast_speech_start(speech); + ast_test_validate_cleanup(test, !ast_speech_dtmf(speech, "1"), res, cleanup); + ast_test_validate_cleanup(test, !ast_speech_change(speech, "foo", "bar"), res, cleanup); + ast_test_validate_cleanup(test, !ast_speech_change_results_type( + speech, AST_SPEECH_RESULTS_TYPE_NBEST), res, cleanup); + + ast_test_validate_cleanup(test, !ast_speech_get_setting( + speech, "foo", buf, sizeof(buf)), res, cleanup); + ast_test_validate_cleanup(test, !strcmp(buf, "bar"), res, cleanup); + + ast_test_validate_cleanup(test, (results = ast_speech_results_get(speech)), res, cleanup); + ast_test_validate_cleanup(test, !strcmp(results->text, TEST_SPEECH_RESULTS_TEXT), res, cleanup); + ast_test_validate_cleanup(test, results->score == TEST_SPEECH_RESULTS_SCORE, res, cleanup); + ast_test_validate_cleanup(test, !strcmp(results->grammar, TEST_SPEECH_RESULTS_GRAMMAR), res, cleanup); + ast_test_validate_cleanup(test, results->nbest_num == TEST_SPEECH_RESULTS_BEST, res, cleanup); + +cleanup: + if (speech) { + ast_speech_destroy(speech); + } + ast_websocket_remove_protocol("_aeap_test_speech_", speech_test_server_cb); + + return res; +} + +static struct ast_http_server *http_server; + +static int load_module(void) +{ + if (!(http_server = ast_http_test_server_get("aeap transport http server", NULL))) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(res_speech_aeap_test); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(res_speech_aeap_test); + + ast_http_test_server_discard(http_server); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk External Application Protocol Speech test(s)", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .requires = "res_speech_aeap", +); diff --git a/tests/test_aeap_transaction.c b/tests/test_aeap_transaction.c new file mode 100644 index 0000000000000000000000000000000000000000..f1879adb43d6297a9984a4a06d70127bb99b36ba --- /dev/null +++ b/tests/test_aeap_transaction.c @@ -0,0 +1,179 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>res_aeap</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pthread.h> + +#include "asterisk/lock.h" +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/res_aeap.h" +#include "asterisk/res_aeap_message.h" + +#include "../res/res_aeap/general.h" +#include "../res/res_aeap/transaction.h" + +#define CATEGORY "/res/aeap/transaction/" + +#define AEAP_TRANSACTION_ID "foo" + +static void handle_timeout(struct ast_aeap *aeap, struct ast_aeap_message *msg, void *obj) +{ + int *passed = obj; + + ++*passed; +} + +static void *end_transaction(void *data) +{ + /* Delay a second before ending transaction */ + struct timespec delay = { 1, 0 }; + int *passed = aeap_transaction_user_obj(data); + + while (nanosleep(&delay, &delay)); + + ++*passed; + aeap_transaction_end(data, 0); + + return NULL; +} + +static enum ast_test_result_state exec(struct ast_test *test, + struct ast_aeap_tsx_params *params) +{ + pthread_t thread_id = AST_PTHREADT_NULL; + struct ao2_container *tsxs = NULL; + struct aeap_transaction *tsx = NULL; + enum ast_test_result_state res = AST_TEST_FAIL; + int passed = 0; + + tsxs = aeap_transactions_create(); + if (!tsxs) { + ast_test_status_update(test, "Failed to create transactions object\n"); + goto exec_cleanup; + } + + params->wait = 1; + params->obj = &passed; + + tsx = aeap_transaction_create_and_add(tsxs, AEAP_TRANSACTION_ID, params, NULL); + if (!tsx) { + ast_test_status_update(test, "Failed to create transaction object\n"); + goto exec_cleanup; + } + + if (ast_pthread_create(&thread_id, NULL, end_transaction, ao2_bump(tsx))) { + ast_test_status_update(test, "Failed to create response thread\n"); + ao2_ref(tsx, -1); + goto exec_cleanup; + } + + if (aeap_transaction_start(tsx)) { + ast_test_status_update(test, "Failed to start transaction request\n"); + goto exec_cleanup; + } + + if (passed == 1) { + res = AST_TEST_PASS; + } + +exec_cleanup: + + if (thread_id != AST_PTHREADT_NULL) { + pthread_cancel(thread_id); + pthread_join(thread_id, NULL); + } + + aeap_transaction_end(tsx, 0); + ao2_cleanup(tsxs); + + return res; +} + +AST_TEST_DEFINE(transaction_exec) +{ + struct ast_aeap_tsx_params params = { + .timeout = 5000, /* Give plenty of time for test thread to end */ + }; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test creating a basic AEAP transaction request"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return exec(test, ¶ms); +} + +AST_TEST_DEFINE(transaction_exec_timeout) +{ + struct ast_aeap_tsx_params params = { + .timeout = 100, /* Ensure timeout occurs before test thread ends */ + .on_timeout = handle_timeout, + }; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test creating a AEAP transaction request that times out"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return exec(test, ¶ms); +} + +static int load_module(void) +{ + AST_TEST_REGISTER(transaction_exec); + AST_TEST_REGISTER(transaction_exec_timeout); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(transaction_exec_timeout); + AST_TEST_UNREGISTER(transaction_exec); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk External Application Protocol Transaction Tests", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .requires = "res_aeap", +); diff --git a/tests/test_aeap_transport.c b/tests/test_aeap_transport.c new file mode 100644 index 0000000000000000000000000000000000000000..675ecf2088ae105476313177508809963dc77ae9 --- /dev/null +++ b/tests/test_aeap_transport.c @@ -0,0 +1,249 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * Kevin Harwell <kharwell@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. + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>res_aeap</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include "asterisk/http.h" +#include "asterisk/test.h" +#include "asterisk/module.h" + +#include "../res/res_aeap/transport.h" + +#define CATEGORY "/res/aeap/transport/" + +#define ADDR "127.0.0.1:8088" +#define TRANSPORT_URL "ws://" ADDR "/ws" +#define TRANSPORT_URL_INVALID "ws://" ADDR "/invalid" +#define TRANSPORT_PROTOCOL "echo" +#define TRANSPORT_PROTOCOL_INVALID "invalid" +#define TRANSPORT_TIMEOUT 2000 + +AST_TEST_DEFINE(transport_create_invalid) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test creating an AEAP invalid transport type"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Transport is expected to be NULL here */ + ast_test_validate(test, !(transport = aeap_transport_create("invalid"))); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(transport_create) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test creating an AEAP transport"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Type is based off the scheme, so just pass in the URL here */ + ast_test_validate(test, (transport = aeap_transport_create(TRANSPORT_URL))); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(transport_connect) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test connecting to an AEAP transport"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Type is based off the scheme, so just pass in the URL for the type */ + ast_test_validate(test, (transport = aeap_transport_create_and_connect( + TRANSPORT_URL, TRANSPORT_URL, TRANSPORT_PROTOCOL, TRANSPORT_TIMEOUT))); + + ast_test_validate(test, aeap_transport_is_connected(transport)); + ast_test_validate(test, !aeap_transport_disconnect(transport)); + ast_test_validate(test, !aeap_transport_is_connected(transport)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(transport_connect_fail) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test connecting failure for an AEAP transport"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Test invalid address */ + ast_test_validate(test, (transport = aeap_transport_create(TRANSPORT_URL))); + + ast_test_validate(test, aeap_transport_connect(transport, + TRANSPORT_URL_INVALID, TRANSPORT_PROTOCOL, TRANSPORT_TIMEOUT)); + + ast_test_validate(test, !aeap_transport_is_connected(transport)); + + aeap_transport_destroy(transport); + + /* Test invalid protocol */ + ast_test_validate(test, (transport = aeap_transport_create(TRANSPORT_URL))); + + ast_test_validate(test, aeap_transport_connect(transport, + TRANSPORT_URL, TRANSPORT_PROTOCOL_INVALID, TRANSPORT_TIMEOUT)); + + ast_test_validate(test, !aeap_transport_is_connected(transport)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(transport_binary) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + int num = 38; + enum AST_AEAP_DATA_TYPE rtype; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test binary I/O from an AEAP transport"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (transport = aeap_transport_create_and_connect( + TRANSPORT_URL, TRANSPORT_URL, TRANSPORT_PROTOCOL, TRANSPORT_TIMEOUT))); + + ast_test_validate(test, aeap_transport_write(transport, &num, sizeof(num), + AST_AEAP_DATA_TYPE_BINARY) == sizeof(num)); + ast_test_validate(test, aeap_transport_read(transport, &num, + sizeof(num), &rtype) == sizeof(num)); + ast_test_validate(test, rtype == AST_AEAP_DATA_TYPE_BINARY); + ast_test_validate(test, num == 38); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(transport_string) +{ + RAII_VAR(struct aeap_transport *, transport, NULL, aeap_transport_destroy); + char buf[16]; + enum AST_AEAP_DATA_TYPE rtype; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->explicit_only = 0; + info->category = CATEGORY; + info->summary = "test string I/O from an AEAP transport"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (transport = aeap_transport_create_and_connect( + TRANSPORT_URL, TRANSPORT_URL, TRANSPORT_PROTOCOL, TRANSPORT_TIMEOUT))); + + ast_test_validate(test, aeap_transport_write(transport, "foo bar baz", 11, + AST_AEAP_DATA_TYPE_STRING) == 11); + ast_test_validate(test, aeap_transport_read(transport, buf, + sizeof(buf) / sizeof(char), &rtype) == 11); + ast_test_validate(test, rtype == AST_AEAP_DATA_TYPE_STRING); + ast_test_validate(test, !strcmp(buf, "foo bar baz")); + + return AST_TEST_PASS; +} + +static struct ast_http_server *http_server; + +static int load_module(void) +{ + if (!(http_server = ast_http_test_server_get("aeap transport http server", NULL))) { + return AST_MODULE_LOAD_DECLINE; + } + + AST_TEST_REGISTER(transport_string); + AST_TEST_REGISTER(transport_binary); + AST_TEST_REGISTER(transport_connect_fail); + AST_TEST_REGISTER(transport_connect); + AST_TEST_REGISTER(transport_create); + AST_TEST_REGISTER(transport_create_invalid); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(transport_string); + AST_TEST_UNREGISTER(transport_binary); + AST_TEST_UNREGISTER(transport_connect_fail); + AST_TEST_UNREGISTER(transport_connect); + AST_TEST_UNREGISTER(transport_create); + AST_TEST_UNREGISTER(transport_create_invalid); + + ast_http_test_server_discard(http_server); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk External Application Protocol Transport Tests", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .requires = "res_aeap", +);