diff --git a/include/asterisk/http.h b/include/asterisk/http.h index 0642cfa9bf77935e9dce0b5513e14aeee27f602a..35c8b22bdfb0a6df7089712d794ce6b0e7e9eb9a 100644 --- a/include/asterisk/http.h +++ b/include/asterisk/http.h @@ -225,4 +225,63 @@ struct ast_json; struct ast_json *ast_http_get_json( struct ast_tcptls_session_instance *ser, struct ast_variable *headers); +/*!\brief Parse the http response status line. + * + * \param buf the http response line information + * \param version the expected http version (e.g. HTTP/1.1) + * \param code the expected status code + * \return -1 if version didn't match or status code conversion fails. + * \return status code (>0) + * \since 13 + */ +int ast_http_response_status_line(const char *buf, const char *version, int code); + +/*!\brief Parse a header into the given name/value strings. + * + * \note This modifies the given buffer and the out parameters point (not + * allocated) to the start of the header name and header value, + * respectively. + * + * \param buf a string containing the name/value to point to + * \param name out parameter pointing to the header name + * \param value out parameter pointing to header value + * \return -1 if buf is empty + * \return 0 if buf could be separated into into name and value + * \return 1 if name or value portion don't exist + * \since 13 + */ +int ast_http_header_parse(char *buf, char **name, char **value); + +/*!\brief Check if the header and value match (case insensitive) their + * associated expected values. + * + * \param name header name to check + * \param expected_name the expected name of the header + * \param value header value to check + * \param expected_value the expected value of the header + * \return 0 if the name and expected name do not match + * \return -1 if the value and expected value do not match + * \return 1 if the both the name and value match their expected value + * \since 13 + */ +int ast_http_header_match(const char *name, const char *expected_name, + const char *value, const char *expected_value); + +/*!\brief Check if the header name matches the expected header name. If so, + * then check to see if the value can be located in the expected value. + * + * \note Both header and value checks are case insensitive. + * + * \param name header name to check + * \param expected_name the expected name of the header + * \param value header value to check if in expected value + * \param expected_value the expected value(s) + * \return 0 if the name and expected name do not match + * \return -1 if the value and is not in the expected value + * \return 1 if the name matches expected name and value is in expected value + * \since 13 + */ +int ast_http_header_match_in(const char *name, const char *expected_name, + const char *value, const char *expected_value); + #endif /* _ASTERISK_SRV_H */ diff --git a/include/asterisk/http_websocket.h b/include/asterisk/http_websocket.h index 10cb9a023230ebad8e5d8893af700f1b64282719..d95e6068ebd8b51ef365d147a3e3c3d3e4482448 100644 --- a/include/asterisk/http_websocket.h +++ b/include/asterisk/http_websocket.h @@ -26,7 +26,15 @@ /*! * \file http_websocket.h - * \brief Support for WebSocket connections within the Asterisk HTTP server. + * \brief Support for WebSocket connections within the Asterisk HTTP server and client + * WebSocket connections to a server. + * + * Supported WebSocket versions in server implementation: + * Version 7 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 + * Version 8 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 + * Version 13 defined in specification http://tools.ietf.org/html/rfc6455 + * Supported WebSocket versions in client implementation: + * Version 13 defined in specification http://tools.ietf.org/html/rfc6455 * * \author Joshua Colp <jcolp@digium.com> * @@ -145,6 +153,20 @@ AST_OPTIONAL_API(int, ast_websocket_server_remove_protocol, (struct ast_websocke */ AST_OPTIONAL_API(int, ast_websocket_read, (struct ast_websocket *session, char **payload, uint64_t *payload_len, enum ast_websocket_opcode *opcode, int *fragmented), { errno = ENOSYS; return -1;}); +/*! + * \brief Read a WebSocket frame containing string data. + * + * \param ws pointer to the websocket + * \param buf string buffer to populate with data read from socket + * \retval -1 on error + * \retval number of bytes read on success + * + * \note Once an AST_WEBSOCKET_OPCODE_CLOSE opcode is received the socket will be closed + */ +AST_OPTIONAL_API(int, ast_websocket_read_string, + (struct ast_websocket *ws, struct ast_str **buf), + { errno = ENOSYS; return -1;}); + /*! * \brief Construct and transmit a WebSocket frame * @@ -158,6 +180,17 @@ AST_OPTIONAL_API(int, ast_websocket_read, (struct ast_websocket *session, char * */ AST_OPTIONAL_API(int, ast_websocket_write, (struct ast_websocket *session, enum ast_websocket_opcode opcode, char *payload, uint64_t actual_length), { errno = ENOSYS; return -1;}); +/*! + * \brief Construct and transmit a WebSocket frame containing string data. + * + * \param ws pointer to the websocket + * \param buf string data to write to socket + * \retval 0 if successfully written + * \retval -1 if error occurred + */ +AST_OPTIONAL_API(int, ast_websocket_write_string, + (struct ast_websocket *ws, const struct ast_str *buf), + { errno = ENOSYS; return -1;}); /*! * \brief Close a WebSocket session by sending a message with the CLOSE opcode and an optional code * @@ -234,4 +267,59 @@ AST_OPTIONAL_API(int, ast_websocket_is_secure, (struct ast_websocket *session), */ AST_OPTIONAL_API(int, ast_websocket_set_nonblock, (struct ast_websocket *session), { errno = ENOSYS; return -1;}); +/*! + * \brief Result code for a websocket client. + */ +enum ast_websocket_result { + WS_OK, + WS_ALLOCATE_ERROR, + WS_KEY_ERROR, + WS_URI_PARSE_ERROR, + WS_URI_RESOLVE_ERROR, + WS_BAD_STATUS, + WS_INVALID_RESPONSE, + WS_BAD_REQUEST, + WS_URL_NOT_FOUND, + WS_HEADER_MISMATCH, + WS_HEADER_MISSING, + WS_NOT_SUPPORTED, + WS_WRITE_ERROR, + WS_CLIENT_START_ERROR, +}; + +/*! + * \brief Create, and connect, a websocket client. + * + * \detail If the client websocket successfully connects, then the accepted protocol + * can be checked via a call to ast_websocket_client_accept_protocol. + * + * \note While connecting this *will* block until a response is + * received from the remote host. + * \note Expected uri form: ws[s]://<address>[:port][/<path>] The address (can be a + * host name) and port are parsed out and used to connect to the remote server. + * If multiple IPs are returned during address resolution then the first one is + * chosen. + * + * \param uri uri to connect to + * \param protocols a comma separated string of supported protocols + * \param tls_cfg secure websocket credentials + * \param result result code set on client failure + * \retval a client websocket. + * \retval NULL if object could not be created or connected + * \since 13 + */ +AST_OPTIONAL_API(struct ast_websocket *, ast_websocket_client_create, + (const char *uri, const char *protocols, + struct ast_tls_config *tls_cfg, + enum ast_websocket_result *result), { return NULL;}); + +/*! + * \brief Retrieve the server accepted sub-protocol on the client. + * + * \param ws the websocket client + * \retval the accepted client sub-protocol. + * \since 13 + */ +AST_OPTIONAL_API(const char *, ast_websocket_client_accept_protocol, + (struct ast_websocket *ws), { return NULL;}); #endif diff --git a/include/asterisk/uri.h b/include/asterisk/uri.h new file mode 100644 index 0000000000000000000000000000000000000000..225d8c8d7f9bff010df8c2336b7de8eef778981e --- /dev/null +++ b/include/asterisk/uri.h @@ -0,0 +1,181 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2014, Digium, Inc. + * + * Kevin Harwell <kharwell@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef _ASTERISK_URI_H +#define _ASTERISK_URI_H + +/*! \brief Opaque structure that stores uri information. */ +struct ast_uri; + +/*! + * \brief Create a uri with the given parameters + * + * \param scheme the uri scheme (ex: http) + * \param user_info user credentials (ex: <name>@<pass>) + * \param host host name or ip address + * \param port the port + * \param path the path + * \param query query parameters + * \return a structure containing parsed uri data. + * \return \c NULL on error + * \since 13 + */ +struct ast_uri *ast_uri_create(const char *scheme, const char *user_info, + const char *host, const char *port, + const char *path, const char *query); + +/*! + * \brief Copy the given uri replacing any value in the new uri with + * any given. + * + * \param uri the uri object to copy + * \param scheme the uri scheme (ex: http) + * \param user_info user credentials (ex: <name>@<pass>) + * \param host host name or ip address + * \param port the port + * \param path the path + * \param query query parameters + * \return a copy of the given uri with specified values replaced. + * \return \c NULL on error + * \since 13 + */ +struct ast_uri *ast_uri_copy_replace(const struct ast_uri *uri, const char *scheme, + const char *user_info, const char *host, + const char *port, const char *path, + const char *query); +/*! + * \brief Retrieve the uri scheme. + * + * \return the uri scheme. + * \since 13 + */ +const char *ast_uri_scheme(const struct ast_uri *uri); + +/*! + * \brief Retrieve the uri user information. + * + * \return the uri user information. + * \since 13 + */ +const char *ast_uri_user_info(const struct ast_uri *uri); + +/*! + * \brief Retrieve the uri host. + * + * \return the uri host. + * \since 13 + */ +const char *ast_uri_host(const struct ast_uri *uri); + +/*! + * \brief Retrieve the uri port + * + * \return the uri port. + * \since 13 + */ +const char *ast_uri_port(const struct ast_uri *uri); + +/*! + * \brief Retrieve the uri path. + * + * \return the uri path. + * \since 13 + */ +const char *ast_uri_path(const struct ast_uri *uri); + +/*! + * \brief Retrieve the uri query parameters. + * + * \return the uri query parameters. + * \since 13 + */ +const char *ast_uri_query(const struct ast_uri *uri); + +/*! + * \brief Retrieve if the uri is of a secure type + * + * \note Secure types are recognized by an 's' at the end + * of the scheme. + * + * \return True if secure, False otherwise. + * \since 13 + */ +const int ast_uri_is_secure(const struct ast_uri *uri); + +/*! + * \brief Parse the given uri into a structure. + * + * \note Expects the following form: + * <scheme>://[user:pass@]<host>[:port][/<path>] + * + * \param uri a string uri to parse + * \return a structure containing parsed uri data. + * \return \c NULL on error + * \since 13 + */ +struct ast_uri *ast_uri_parse(const char *uri); + +/*! + * \brief Parse the given http uri into a structure. + * + * \note Expects the following form: + * [http[s]://][user:pass@]<host>[:port][/<path>] + * + * \note If no scheme is given it defaults to 'http' and if + * no port is specified it will default to 443 if marked + * secure, otherwise to 80. + * + * \param uri an http string uri to parse + * \return a structure containing parsed http uri data. + * \return \c NULL on error + * \since 13 + */ +struct ast_uri *ast_uri_parse_http(const char *uri); + +/*! + * \brief Parse the given websocket uri into a structure. + * + * \note Expects the following form: + * [ws[s]://][user:pass@]<host>[:port][/<path>] + * + * \note If no scheme is given it defaults to 'ws' and if + * no port is specified it will default to 443 if marked + * secure, otherwise to 80. + * + * \param uri a websocket string uri to parse + * \return a structure containing parsed http uri data. + * \return \c NULL on error + * \since 13 + */ +struct ast_uri *ast_uri_parse_websocket(const char *uri); + +/*! + * \brief Retrieve a string of the host and port. + * + * \detail Combine the host and port (<host>:<port>) if the port + * is available, otherwise just return the host. + * + * \note Caller is responsible for release the returned string. + * + * \param uri the uri object + * \return a string value of the host and optional port. + * \since 13 + */ +char *ast_uri_make_host_with_port(const struct ast_uri *uri); + +#endif /* _ASTERISK_URI_H */ diff --git a/main/http.c b/main/http.c index 783a34cfeff6f702ecd9164366a9742c4ecc4670..0c9395d9c872e9f4b3d9849032618bdcb9a22826 100644 --- a/main/http.c +++ b/main/http.c @@ -1176,6 +1176,113 @@ struct ast_http_auth *ast_http_get_auth(struct ast_variable *headers) return NULL; } +int ast_http_response_status_line(const char *buf, const char *version, int code) +{ + int status_code; + size_t size = strlen(version); + + if (strncmp(buf, version, size) || buf[size] != ' ') { + ast_log(LOG_ERROR, "HTTP version not supported - " + "expected %s\n", version); + return -1; + } + + /* skip to status code (version + space) */ + buf += size + 1; + + if (sscanf(buf, "%d", &status_code) != 1) { + ast_log(LOG_ERROR, "Could not read HTTP status code - " + "%s\n", buf); + return -1; + } + + return status_code; +} + +static void remove_excess_lws(char *s) +{ + char *p, *res = s; + char *buf = ast_malloc(strlen(s) + 1); + char *buf_end; + + if (!buf) { + return; + } + + buf_end = buf; + + while (*s && *(s = ast_skip_blanks(s))) { + p = s; + s = ast_skip_nonblanks(s); + + if (buf_end != buf) { + *buf_end++ = ' '; + } + + memcpy(buf_end, p, s - p); + buf_end += s - p; + } + *buf_end = '\0'; + /* safe since buf will always be less than or equal to res */ + strcpy(res, buf); + ast_free(buf); +} + +int ast_http_header_parse(char *buf, char **name, char **value) +{ + ast_trim_blanks(buf); + if (ast_strlen_zero(buf)) { + return -1; + } + + *value = buf; + *name = strsep(value, ":"); + if (!*value) { + return 1; + } + + *value = ast_skip_blanks(*value); + if (ast_strlen_zero(*value) || ast_strlen_zero(*name)) { + return 1; + } + + remove_excess_lws(*value); + return 0; +} + +int ast_http_header_match(const char *name, const char *expected_name, + const char *value, const char *expected_value) +{ + if (strcasecmp(name, expected_name)) { + /* no value to validate if names don't match */ + return 0; + } + + if (strcasecmp(value, expected_value)) { + ast_log(LOG_ERROR, "Invalid header value - expected %s " + "received %s", value, expected_value); + return -1; + } + return 1; +} + +int ast_http_header_match_in(const char *name, const char *expected_name, + const char *value, const char *expected_value) +{ + if (strcasecmp(name, expected_name)) { + /* no value to validate if names don't match */ + return 0; + } + + if (!strcasestr(expected_value, value)) { + ast_log(LOG_ERROR, "Header '%s' - could not locate '%s' " + "in '%s'\n", name, value, expected_value); + return -1; + + } + return 1; +} + /*! Limit the number of request headers in case the sender is being ridiculous. */ #define MAX_HTTP_REQUEST_HEADERS 100 diff --git a/main/uri.c b/main/uri.c new file mode 100644 index 0000000000000000000000000000000000000000..6642d843b2b2bdec44834ce6f84b32b5c8478d61 --- /dev/null +++ b/main/uri.c @@ -0,0 +1,321 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2014, Digium, Inc. + * + * Kevin Harwell <kharwell@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include "asterisk/astobj2.h" +#include "asterisk/strings.h" +#include "asterisk/uri.h" + +#ifdef HAVE_URIPARSER +#include <uriparser/Uri.h> +#endif + +/*! \brief Stores parsed uri information */ +struct ast_uri { + /*! scheme (e.g. http, https, ws, wss, etc...) */ + char *scheme; + /*! username:password */ + char *user_info; + /*! host name or address */ + char *host; + /*! associated port */ + char *port; + /*! path info following host[:port] */ + char *path; + /*! query information */ + char *query; + /*! storage for uri string */ + char uri[0]; +}; + +/*! + * \brief Construct a uri object with the given values. + * + * \note The size parameters [should] include room for the string terminator + * (strlen(<param>) + 1). For instance, if a scheme of 'http' is given + * then the 'scheme_size' should be equal to 5. + */ +static struct ast_uri *ast_uri_create_( + const char *scheme, unsigned int scheme_size, + const char *user_info, unsigned int user_info_size, + const char *host, unsigned int host_size, + const char *port, unsigned int port_size, + const char *path, unsigned int path_size, + const char *query, unsigned int query_size) +{ +#define SET_VALUE(param, field, size) \ + do { if (param) { \ + ast_copy_string(p, param, size); \ + field = p; \ + p += size; } } while (0) + + char *p; + struct ast_uri *res = ao2_alloc( + sizeof(*res) + scheme_size + user_info_size + host_size + + port_size + path_size + query_size, NULL); + + if (!res) { + ast_log(LOG_ERROR, "Unable to create URI object\n"); + return NULL; + } + + p = res->uri; + SET_VALUE(scheme, res->scheme, scheme_size); + SET_VALUE(user_info, res->user_info, user_info_size); + SET_VALUE(host, res->host, host_size); + SET_VALUE(port, res->port, port_size); + SET_VALUE(path, res->path, path_size); + SET_VALUE(query, res->query, query_size); + return res; +} + +struct ast_uri *ast_uri_create(const char *scheme, const char *user_info, + const char *host, const char *port, + const char *path, const char *query) +{ + return ast_uri_create_( + scheme, scheme ? strlen(scheme) + 1 : 0, + user_info, user_info ? strlen(user_info) + 1 : 0, + host, host ? strlen(host) + 1 : 0, + port, port ? strlen(port) + 1 : 0, + path, path ? strlen(path) + 1 : 0, + query, query ? strlen(query) + 1 : 0); +} + +struct ast_uri *ast_uri_copy_replace(const struct ast_uri *uri, const char *scheme, + const char *user_info, const char *host, + const char *port, const char *path, + const char *query) +{ + return ast_uri_create( + scheme ? scheme : uri->scheme, + user_info ? user_info : uri->user_info, + host ? host : uri->host, + port ? port : uri->port, + path ? path : uri->path, + query ? query : uri->query); +} + +const char *ast_uri_scheme(const struct ast_uri *uri) +{ + return uri->scheme; +} + +const char *ast_uri_user_info(const struct ast_uri *uri) +{ + return uri->user_info; +} + +const char *ast_uri_host(const struct ast_uri *uri) +{ + return uri->host; +} + +const char *ast_uri_port(const struct ast_uri *uri) +{ + return uri->port; +} + +const char *ast_uri_path(const struct ast_uri *uri) +{ + return uri->path; +} + +const char *ast_uri_query(const struct ast_uri *uri) +{ + return uri->query; +} + +const int ast_uri_is_secure(const struct ast_uri *uri) +{ + return ast_strlen_zero(uri->scheme) ? 0 : + *(uri->scheme + strlen(uri->scheme) - 1) == 's'; +} + +#ifdef HAVE_URIPARSER +struct ast_uri *ast_uri_parse(const char *uri) +{ + UriParserStateA state; + UriUriA uria; + struct ast_uri *res; + unsigned int scheme_size, user_info_size, host_size; + unsigned int port_size, path_size, query_size; + const char *path_start, *path_end; + + state.uri = &uria; + if (uriParseUriA(&state, uri) != URI_SUCCESS) { + ast_log(LOG_ERROR, "Unable to parse URI %s\n", uri); + uriFreeUriMembersA(&uria); + return NULL; + } + + scheme_size = uria.scheme.first ? + uria.scheme.afterLast - uria.scheme.first + 1 : 0; + user_info_size = uria.userInfo.first ? + uria.userInfo.afterLast - uria.userInfo.first + 1 : 0; + host_size = uria.hostText.first ? + uria.hostText.afterLast - uria.hostText.first + 1 : 0; + port_size = uria.portText.first ? + uria.portText.afterLast - uria.portText.first + 1 : 0; + + path_start = uria.pathHead && uria.pathHead->text.first ? + uria.pathHead->text.first : NULL; + path_end = path_start ? uria.pathTail->text.afterLast : NULL; + path_size = path_end ? path_end - path_start + 1 : 0; + + query_size = uria.query.first ? + uria.query.afterLast - uria.query.first + 1 : 0; + + res = ast_uri_create_(uria.scheme.first, scheme_size, + uria.userInfo.first, user_info_size, + uria.hostText.first, host_size, + uria.portText.first, port_size, + path_start, path_size, + uria.query.first, query_size); + uriFreeUriMembersA(&uria); + return res; +} +#else +struct ast_uri *ast_uri_parse(const char *uri) +{ +#define SET_VALUES(value) \ + value = uri; \ + size_##value = p - uri + 1; \ + uri = p + 1; + + const char *p, *scheme = NULL, *user_info = NULL, *host = NULL; + const char *port = NULL, *path = NULL, *query = NULL; + unsigned int size_scheme = 0, size_user_info = 0, size_host = 0; + unsigned int size_port = 0, size_path = 0, size_query = 0; + + if ((p = strstr(uri, "://"))) { + scheme = uri; + size_scheme = p - uri + 1; + uri = p + 3; + } + + if ((p = strchr(uri, '@'))) { + SET_VALUES(user_info); + } + + if ((p = strchr(uri, ':'))) { + SET_VALUES(host); + } + + if ((p = strchr(uri, '/'))) { + if (!host) { + SET_VALUES(host); + } else { + SET_VALUES(port); + } + } + + if ((p = strchr(uri, '?'))) { + query = p + 1; + size_query = strlen(query) + 1; + } + + if (!host) { + SET_VALUES(host); + } else if (*(uri - 1) == ':') { + SET_VALUES(port); + } else if (*(uri - 1) == '/') { + SET_VALUES(path); + } + + return ast_uri_create_(scheme, size_scheme, + user_info, size_user_info, + host, size_host, + port, size_port, + path, size_path, + query, size_query); +} +#endif + +static struct ast_uri *uri_parse_and_default(const char *uri, const char *scheme, + const char *port, const char *secure_port) +{ + struct ast_uri *res; + int len = strlen(scheme); + + if (!strncmp(uri, scheme, len)) { + res = ast_uri_parse(uri); + } else { + /* make room for <scheme>:// */ + char *with_scheme = ast_malloc(len + strlen(uri) + 4); + if (!with_scheme) { + ast_log(LOG_ERROR, "Unable to allocate uri '%s' with " + "scheme '%s'", uri, scheme); + return NULL; + } + + /* safe - 'with_scheme' created with size equal to len of + scheme plus length of uri plus space for extra characters + '://' and terminator */ + sprintf(with_scheme, "%s://%s", scheme, uri); + res = ast_uri_parse(with_scheme); + ast_free(with_scheme); + } + + if (res && ast_strlen_zero(ast_uri_port(res))) { + /* default the port if not given */ + struct ast_uri *tmp = ast_uri_copy_replace( + res, NULL, NULL, NULL, + ast_uri_is_secure(res) ? secure_port : port, + NULL, NULL); + ao2_ref(res, -1); + res = tmp; + } + return res; +} + +struct ast_uri *ast_uri_parse_http(const char *uri) +{ + return uri_parse_and_default(uri, "http", "80", "443"); +} + +struct ast_uri *ast_uri_parse_websocket(const char *uri) +{ + return uri_parse_and_default(uri, "ws", "80", "443"); +} + +char *ast_uri_make_host_with_port(const struct ast_uri *uri) +{ + int host_size = ast_uri_host(uri) ? + strlen(ast_uri_host(uri)) : 0; + /* if there is a port +1 for the colon */ + int port_size = ast_uri_port(uri) ? + strlen(ast_uri_port(uri)) + 1 : 0; + char *res = ast_malloc(host_size + port_size + 1); + + if (!res) { + return NULL; + } + + memcpy(res, ast_uri_host(uri), host_size); + + if (ast_uri_port(uri)) { + res[host_size] = ':'; + memcpy(res + host_size + 1, + ast_uri_port(uri), port_size); + } + + res[host_size + port_size + 1] = '\0'; + return res; +} diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c index cdb639f485ac60e95e78f57f29d98ca1c960d90c..07cb6b7be990e98332702a3afe788bbb11d3a505 100644 --- a/res/res_http_websocket.c +++ b/res/res_http_websocket.c @@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/strings.h" #include "asterisk/file.h" #include "asterisk/unaligned.h" +#include "asterisk/uri.h" #define AST_API_MODULE #include "asterisk/http_websocket.h" @@ -44,6 +45,9 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") /*! \brief GUID used to compute the accept key, defined in the specifications */ #define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +/*! \brief Length of a websocket's client key */ +#define CLIENT_KEY_SIZE 16 + /*! \brief Number of buckets for registered protocols */ #define MAX_PROTOCOL_BUCKETS 7 @@ -80,6 +84,7 @@ struct ast_websocket { unsigned int secure:1; /*!< Bit to indicate that the transport is secure */ unsigned int closing:1; /*!< Bit to indicate that the session is in the process of being closed */ unsigned int close_sent:1; /*!< Bit to indicate that the session close opcode has been sent and no further data will be sent */ + struct websocket_client *client; /*!< Client object when connected as a client websocket */ }; /*! \brief Structure definition for protocols */ @@ -165,13 +170,13 @@ static void session_destroy_fn(void *obj) { struct ast_websocket *session = obj; - ast_websocket_close(session, 0); - if (session->f) { + ast_websocket_close(session, 0); fclose(session->f); ast_verb(2, "WebSocket connection from '%s' closed\n", ast_sockaddr_stringify(&session->address)); } + ao2_cleanup(session->client); ast_free(session->payload); } @@ -578,6 +583,19 @@ static struct websocket_protocol *one_protocol( return ao2_callback(server->protocols, OBJ_NOLOCK, NULL, NULL); } +static char *websocket_combine_key(const char *key, char *res, int res_size) +{ + char *combined; + unsigned combined_length = strlen(key) + strlen(WEBSOCKET_GUID) + 1; + uint8_t sha[20]; + + combined = ast_alloca(combined_length); + snprintf(combined, combined_length, "%s%s", key, WEBSOCKET_GUID); + ast_sha1_hash_uint(sha, combined); + ast_base64encode(res, (const unsigned char*)sha, 20, res_size); + return res; +} + int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_vars, struct ast_variable *headers) { struct ast_variable *v; @@ -662,15 +680,9 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan /* Determine how to respond depending on the version */ if (version == 7 || version == 8 || version == 13) { - /* Version 7 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 */ - /* Version 8 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 */ - /* Version 13 defined in specification http://tools.ietf.org/html/rfc6455 */ - char *combined, base64[64]; - unsigned combined_length; - uint8_t sha[20]; - - combined_length = (key ? strlen(key) : 0) + strlen(WEBSOCKET_GUID) + 1; - if (!key || combined_length > 8192) { /* no stack overflows please */ + char base64[64]; + + if (!key || strlen(key) + strlen(WEBSOCKET_GUID) + 1 > 8192) { /* no stack overflows please */ fputs("HTTP/1.1 400 Bad Request\r\n" "Sec-WebSocket-Version: 7, 8, 13\r\n\r\n", ser->f); ao2_ref(protocol_handler, -1); @@ -686,17 +698,12 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan return 0; } - combined = ast_alloca(combined_length); - snprintf(combined, combined_length, "%s%s", key, WEBSOCKET_GUID); - ast_sha1_hash_uint(sha, combined); - ast_base64encode(base64, (const unsigned char*)sha, 20, sizeof(base64)); - fprintf(ser->f, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: %s\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n", upgrade, - base64); + websocket_combine_key(key, base64, sizeof(base64))); /* RFC 6455, Section 4.1: * @@ -844,6 +851,392 @@ int AST_OPTIONAL_API_NAME(ast_websocket_remove_protocol)(const char *name, ast_w return res; } +/*! \brief Parse the given uri into a path and remote address. + * + * Expected uri form: [ws[s]]://<host>[:port][/<path>] + * + * The returned host will contain the address and optional port while + * path will contain everything after the address/port if included. + */ +static int websocket_client_parse_uri(const char *uri, char **host, char **path) +{ + struct ast_uri *parsed_uri = ast_uri_parse_websocket(uri); + + if (!parsed_uri) { + return -1; + } + + *host = ast_uri_make_host_with_port(parsed_uri); + + if (ast_uri_path(parsed_uri) && !(*path = ast_strdup(ast_uri_path(parsed_uri)))) { + ao2_ref(parsed_uri, -1); + return -1; + } + + ao2_ref(parsed_uri, -1); + return 0; +} + +static void websocket_client_args_destroy(void *obj) +{ + struct ast_tcptls_session_args *args = obj; + + if (args->tls_cfg) { + ast_free(args->tls_cfg->certfile); + ast_free(args->tls_cfg->pvtfile); + ast_free(args->tls_cfg->cipher); + ast_free(args->tls_cfg->cafile); + ast_free(args->tls_cfg->capath); + + ast_ssl_teardown(args->tls_cfg); + } + ast_free(args->tls_cfg); +} + +static struct ast_tcptls_session_args *websocket_client_args_create( + const char *host, struct ast_tls_config *tls_cfg, + enum ast_websocket_result *result) +{ + struct ast_sockaddr *addr; + struct ast_tcptls_session_args *args = ao2_alloc( + sizeof(*args), websocket_client_args_destroy); + + if (!args) { + *result = WS_ALLOCATE_ERROR; + return NULL; + } + + args->accept_fd = -1; + args->tls_cfg = tls_cfg; + args->name = "websocket client"; + + if (!ast_sockaddr_resolve(&addr, host, 0, 0)) { + ast_log(LOG_ERROR, "Unable to resolve address %s\n", + host); + ao2_ref(args, -1); + *result = WS_URI_RESOLVE_ERROR; + return NULL; + } + ast_sockaddr_copy(&args->remote_address, addr); + ast_free(addr); + return args; +} + +static char *websocket_client_create_key(void) +{ + static int encoded_size = CLIENT_KEY_SIZE * 2 * sizeof(char) + 1; + /* key is randomly selected 16-byte base64 encoded value */ + unsigned char key[CLIENT_KEY_SIZE + sizeof(long) - 1]; + char *encoded = ast_malloc(encoded_size); + long i = 0; + + if (!encoded) { + ast_log(LOG_ERROR, "Unable to allocate client websocket key\n"); + return NULL; + } + + while (i < CLIENT_KEY_SIZE) { + long num = ast_random(); + memcpy(key + i, &num, sizeof(long)); + i += sizeof(long); + } + + ast_base64encode(encoded, key, CLIENT_KEY_SIZE, encoded_size); + return encoded; +} + +struct websocket_client { + /*! host portion of client uri */ + char *host; + /*! path for logical websocket connection */ + char *resource_name; + /*! unique key used during server handshaking */ + char *key; + /*! container for registered protocols */ + char *protocols; + /*! the protocol accepted by the server */ + char *accept_protocol; + /*! websocket protocol version */ + int version; + /*! tcptls connection arguments */ + struct ast_tcptls_session_args *args; + /*! tcptls connection instance */ + struct ast_tcptls_session_instance *ser; +}; + +static void websocket_client_destroy(void *obj) +{ + struct websocket_client *client = obj; + + ao2_cleanup(client->ser); + ao2_cleanup(client->args); + + ast_free(client->accept_protocol); + ast_free(client->protocols); + ast_free(client->key); + ast_free(client->resource_name); + ast_free(client->host); +} + +static struct ast_websocket * websocket_client_create( + const char *uri, const char *protocols, struct ast_tls_config *tls_cfg, + enum ast_websocket_result *result) +{ + struct ast_websocket *ws = ao2_alloc(sizeof(*ws), session_destroy_fn); + + if (!ws) { + ast_log(LOG_ERROR, "Unable to allocate websocket\n"); + *result = WS_ALLOCATE_ERROR; + return NULL; + } + + if (!(ws->client = ao2_alloc( + sizeof(*ws->client), websocket_client_destroy))) { + ast_log(LOG_ERROR, "Unable to allocate websocket client\n"); + *result = WS_ALLOCATE_ERROR; + return NULL; + } + + if (!(ws->client->key = websocket_client_create_key())) { + ao2_ref(ws, -1); + *result = WS_KEY_ERROR; + return NULL; + } + + if (websocket_client_parse_uri( + uri, &ws->client->host, &ws->client->resource_name)) { + ao2_ref(ws, -1); + *result = WS_URI_PARSE_ERROR; + return NULL; + } + + if (!(ws->client->args = websocket_client_args_create( + ws->client->host, tls_cfg, result))) { + ao2_ref(ws, -1); + return NULL; + } + ws->client->protocols = ast_strdup(protocols); + + ws->client->version = 13; + ws->opcode = -1; + ws->reconstruct = DEFAULT_RECONSTRUCTION_CEILING; + return ws; +} + +const char * AST_OPTIONAL_API_NAME( + ast_websocket_client_accept_protocol)(struct ast_websocket *ws) +{ + return ws->client->accept_protocol; +} + +static enum ast_websocket_result websocket_client_handle_response_code( + struct websocket_client *client, int response_code) +{ + if (response_code <= 0) { + return WS_INVALID_RESPONSE; + } + + switch (response_code) { + case 101: + return 0; + case 400: + ast_log(LOG_ERROR, "Received response 400 - Bad Request " + "- from %s\n", client->host); + return WS_BAD_REQUEST; + case 404: + ast_log(LOG_ERROR, "Received response 404 - Request URL not " + "found - from %s\n", client->host); + return WS_URL_NOT_FOUND; + } + + ast_log(LOG_ERROR, "Invalid HTTP response code %d from %s\n", + response_code, client->host); + return WS_INVALID_RESPONSE; +} + +static enum ast_websocket_result websocket_client_handshake_get_response( + struct websocket_client *client) +{ + enum ast_websocket_result res; + char buf[4096]; + char base64[64]; + int has_upgrade = 0; + int has_connection = 0; + int has_accept = 0; + int has_protocol = 0; + + if (!fgets(buf, sizeof(buf), client->ser->f)) { + ast_log(LOG_ERROR, "Unable to retrieve HTTP status line."); + return WS_BAD_STATUS; + } + + if ((res = websocket_client_handle_response_code(client, + ast_http_response_status_line( + buf, "HTTP/1.1", 101))) != WS_OK) { + return res; + } + + /* Ignoring line folding - assuming header field values are contained + within a single line */ + while (fgets(buf, sizeof(buf), client->ser->f)) { + char *name, *value; + int parsed = ast_http_header_parse(buf, &name, &value); + + if (parsed < 0) { + break; + } + + if (parsed > 0) { + continue; + } + + if (!has_upgrade && + (has_upgrade = ast_http_header_match( + name, "upgrade", value, "websocket")) < 0) { + return WS_HEADER_MISMATCH; + } else if (!has_connection && + (has_connection = ast_http_header_match( + name, "connection", value, "upgrade")) < 0) { + return WS_HEADER_MISMATCH; + } else if (!has_accept && + (has_accept = ast_http_header_match( + name, "sec-websocket-accept", value, + websocket_combine_key( + client->key, base64, sizeof(base64)))) < 0) { + return WS_HEADER_MISMATCH; + } else if (!has_protocol && + (has_protocol = ast_http_header_match_in( + name, "sec-websocket-protocol", value, client->protocols))) { + if (has_protocol < 0) { + return WS_HEADER_MISMATCH; + } + client->accept_protocol = ast_strdup(value); + } else if (!strcasecmp(name, "sec-websocket-extensions")) { + ast_log(LOG_ERROR, "Extensions received, but not " + "supported by client\n"); + return WS_NOT_SUPPORTED; + } + } + return has_upgrade && has_connection && has_accept ? + WS_OK : WS_HEADER_MISSING; +} + +static enum ast_websocket_result websocket_client_handshake( + struct websocket_client *client) +{ + char protocols[100] = ""; + + if (!ast_strlen_zero(client->protocols)) { + sprintf(protocols, "Sec-WebSocket-Protocol: %s\r\n", + client->protocols); + } + + if (fprintf(client->ser->f, + "GET /%s HTTP/1.1\r\n" + "Sec-WebSocket-Version: %d\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Host: %s\r\n" + "Sec-WebSocket-Key: %s\r\n" + "%s\r\n", + client->resource_name, + client->version, + client->host, + client->key, + protocols) < 0) { + ast_log(LOG_ERROR, "Failed to send handshake.\n"); + return WS_WRITE_ERROR; + } + /* wait for a response before doing anything else */ + return websocket_client_handshake_get_response(client); +} + +static enum ast_websocket_result websocket_client_connect(struct ast_websocket *ws) +{ + enum ast_websocket_result res; + /* create and connect the client - note client_start + releases the session instance on failure */ + if (!(ws->client->ser = ast_tcptls_client_start( + ast_tcptls_client_create(ws->client->args)))) { + return WS_CLIENT_START_ERROR; + } + + if ((res = websocket_client_handshake(ws->client)) != WS_OK) { + ao2_ref(ws->client->ser, -1); + ws->client->ser = NULL; + return res; + } + + ws->f = ws->client->ser->f; + ws->fd = ws->client->ser->fd; + ws->secure = ws->client->ser->ssl ? 1 : 0; + ast_sockaddr_copy(&ws->address, &ws->client->ser->remote_address); + return WS_OK; +} + +struct ast_websocket *AST_OPTIONAL_API_NAME(ast_websocket_client_create) + (const char *uri, const char *protocols, struct ast_tls_config *tls_cfg, + enum ast_websocket_result *result) +{ + struct ast_websocket *ws = websocket_client_create( + uri, protocols, tls_cfg, result); + + if (!ws) { + return NULL; + } + + if ((*result = websocket_client_connect(ws)) != WS_OK) { + ao2_ref(ws, -1); + return NULL; + } + + return ws; +} + +int AST_OPTIONAL_API_NAME(ast_websocket_read_string) + (struct ast_websocket *ws, struct ast_str **buf) +{ + char *payload; + uint64_t payload_len; + enum ast_websocket_opcode opcode; + int fragmented = 1; + + if (!*buf && !(*buf = ast_str_create(512))) { + ast_log(LOG_ERROR, "Client Websocket string read - " + "Unable to allocate string buffer"); + return -1; + } + + while (fragmented) { + if (ast_websocket_read(ws, &payload, &payload_len, + &opcode, &fragmented)) { + ast_log(LOG_ERROR, "Client WebSocket string read - " + "error reading string data\n"); + return -1; + } + + if (opcode == AST_WEBSOCKET_OPCODE_CLOSE) { + return -1; + } + + if (opcode != AST_WEBSOCKET_OPCODE_TEXT) { + ast_log(LOG_ERROR, "Client WebSocket string read - " + "non string data received\n"); + return -1; + } + + ast_str_append(buf, 0, "%s", payload); + } + return ast_str_size(*buf); +} + +int AST_OPTIONAL_API_NAME(ast_websocket_write_string) + (struct ast_websocket *ws, const struct ast_str *buf) +{ + return ast_websocket_write(ws, AST_WEBSOCKET_OPCODE_TEXT, + ast_str_buffer(buf), ast_str_strlen(buf)); +} + static int load_module(void) { websocketuri.data = websocket_server_internal_create(); diff --git a/res/res_http_websocket.exports.in b/res/res_http_websocket.exports.in index de3d02625e96b1d3a6b7734137d5d1e48d22899b..8177fc21b2f4037654ad4b2b406279a3a3eaa311 100644 --- a/res/res_http_websocket.exports.in +++ b/res/res_http_websocket.exports.in @@ -1,22 +1,6 @@ { global: - LINKER_SYMBOL_PREFIX*ast_websocket_add_protocol; - LINKER_SYMBOL_PREFIX*ast_websocket_remove_protocol; - LINKER_SYMBOL_PREFIX*ast_websocket_read; - LINKER_SYMBOL_PREFIX*ast_websocket_write; - LINKER_SYMBOL_PREFIX*ast_websocket_close; - LINKER_SYMBOL_PREFIX*ast_websocket_reconstruct_enable; - LINKER_SYMBOL_PREFIX*ast_websocket_reconstruct_disable; - LINKER_SYMBOL_PREFIX*ast_websocket_ref; - LINKER_SYMBOL_PREFIX*ast_websocket_unref; - LINKER_SYMBOL_PREFIX*ast_websocket_fd; - LINKER_SYMBOL_PREFIX*ast_websocket_remote_address; - LINKER_SYMBOL_PREFIX*ast_websocket_is_secure; - LINKER_SYMBOL_PREFIX*ast_websocket_set_nonblock; - LINKER_SYMBOL_PREFIX*ast_websocket_uri_cb; - LINKER_SYMBOL_PREFIX*ast_websocket_server_create; - LINKER_SYMBOL_PREFIX*ast_websocket_server_add_protocol; - LINKER_SYMBOL_PREFIX*ast_websocket_server_remove_protocol; + LINKER_SYMBOL_PREFIX*ast_websocket_*; local: *; }; diff --git a/tests/test_uri.c b/tests/test_uri.c new file mode 100644 index 0000000000000000000000000000000000000000..92bbb70b0baf3ea6372e1b9e2da49793315e4a88 --- /dev/null +++ b/tests/test_uri.c @@ -0,0 +1,154 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2014, Digium, Inc. + * + * Kevin Harwell <kharwell@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file + * \brief URI Unit Tests + * + * \author Kevin Harwell <kharwell@digium.com> + * + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "") + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/uri.h" + +#define CATEGORY "/main/uri/" + +static const char *scenarios[][7] = { + {"http://name:pass@localhost", "http", "name:pass", "localhost", NULL, NULL, NULL}, + {"http://localhost", "http", NULL, "localhost", NULL, NULL, NULL}, + {"http://localhost:80", "http", NULL, "localhost", "80", NULL, NULL}, + {"http://localhost/path/", "http", NULL, "localhost", NULL, "path/", NULL}, + {"http://localhost/?query", "http", NULL, "localhost", NULL, "", "query"}, + {"http://localhost:80/path", "http", NULL, "localhost", "80", "path", NULL}, + {"http://localhost:80/?query", "http", NULL, "localhost", "80", "", "query"}, + {"http://localhost:80/path?query", "http", NULL, "localhost", "80", "path", "query"}, +}; + +AST_TEST_DEFINE(uri_parse) +{ +#define VALIDATE(value, expected_value) \ + do { ast_test_validate(test, \ + (value == expected_value) || \ + (value && expected_value && \ + !strcmp(value, expected_value))); \ + } while (0) + + int i; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "Uri parsing scenarios"; + info->description = "For each scenario validate result(s)"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + for (i = 0; i < ARRAY_LEN(scenarios); ++i) { + RAII_VAR(struct ast_uri *, uri, NULL, ao2_cleanup); + const char **scenario = scenarios[i]; + + ast_test_validate(test, (uri = ast_uri_parse(scenario[0]))); + VALIDATE(ast_uri_scheme(uri), scenario[1]); + VALIDATE(ast_uri_user_info(uri), scenario[2]); + VALIDATE(ast_uri_host(uri), scenario[3]); + VALIDATE(ast_uri_port(uri), scenario[4]); + VALIDATE(ast_uri_path(uri), scenario[5]); + VALIDATE(ast_uri_query(uri), scenario[6]); + } + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(uri_default_http) +{ + RAII_VAR(struct ast_uri *, uri, NULL, ao2_cleanup); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "parse an http uri with host only"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (uri = ast_uri_parse_http("localhost"))); + ast_test_validate(test, !strcmp(ast_uri_scheme(uri), "http")); + ast_test_validate(test, !strcmp(ast_uri_host(uri), "localhost")); + ast_test_validate(test, !strcmp(ast_uri_port(uri), "80")); + ast_test_validate(test, !ast_uri_is_secure(uri)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(uri_default_http_secure) +{ + RAII_VAR(struct ast_uri *, uri, NULL, ao2_cleanup); + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "parse an https uri with host only"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (uri = ast_uri_parse_http("https://localhost"))); + ast_test_validate(test, !strcmp(ast_uri_scheme(uri), "https")); + ast_test_validate(test, !strcmp(ast_uri_host(uri), "localhost")); + ast_test_validate(test, !strcmp(ast_uri_port(uri), "443")); + ast_test_validate(test, ast_uri_is_secure(uri)); + + return AST_TEST_PASS; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(uri_parse); + AST_TEST_REGISTER(uri_default_http); + AST_TEST_REGISTER(uri_default_http_secure); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(uri_default_http_secure); + AST_TEST_UNREGISTER(uri_default_http); + AST_TEST_UNREGISTER(uri_parse); + return 0; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "URI test module"); diff --git a/tests/test_websocket_client.c b/tests/test_websocket_client.c new file mode 100644 index 0000000000000000000000000000000000000000..e104ed82560332c2c39ae7ebc5000114ec77ad55 --- /dev/null +++ b/tests/test_websocket_client.c @@ -0,0 +1,165 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2014, Digium, Inc. + * + * Kevin Harwell <kharwell@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file + * \brief Websocket Client Unit Tests + * + * \author Kevin Harwell <kharwell@digium.com> + * + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <depend>res_http_websocket</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "") + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/astobj2.h" +#include "asterisk/pbx.h" +#include "asterisk/http_websocket.h" + +#define CATEGORY "/res/websocket/" +#define REMOTE_URL "ws://localhost:8088/ws" + +AST_TEST_DEFINE(websocket_client_create_and_connect) +{ + RAII_VAR(struct ast_websocket *, client, NULL, ao2_cleanup); + + enum ast_websocket_result result; + struct ast_str *write_buf; + struct ast_str *read_buf; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "test creation and connection of a client websocket"; + info->description = "test creation and connection of a client websocket"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + write_buf = ast_str_alloca(20); + read_buf = ast_str_alloca(20); + + ast_test_validate(test, (client = ast_websocket_client_create( + REMOTE_URL, "echo", NULL, &result))); + + ast_str_set(&write_buf, 0, "this is only a test"); + ast_test_validate(test, !ast_websocket_write_string(client, write_buf)); + ast_test_validate(test, ast_websocket_read_string(client, &read_buf) > 0); + ast_test_validate(test, !strcmp(ast_str_buffer(write_buf), ast_str_buffer(read_buf))); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(websocket_client_bad_url) +{ + RAII_VAR(struct ast_websocket *, client, NULL, ao2_cleanup); + enum ast_websocket_result result; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "websocket client - test bad url"; + info->description = "pass a bad url and make sure it fails"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, !(client = ast_websocket_client_create( + "invalid", NULL, NULL, &result))); + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(websocket_client_unsupported_protocol) +{ + RAII_VAR(struct ast_websocket *, client, NULL, ao2_cleanup); + enum ast_websocket_result result; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "websocket client - unsupported protocol"; + info->description = "fails on an unsupported protocol"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, !(client = ast_websocket_client_create( + REMOTE_URL, "unsupported", NULL, &result))); + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(websocket_client_multiple_protocols) +{ + RAII_VAR(struct ast_websocket *, client, NULL, ao2_cleanup); + const char *accept_protocol; + enum ast_websocket_result result; + + switch (cmd) { + case TEST_INIT: + info->name = __func__; + info->category = CATEGORY; + info->summary = "websocket client - test multiple protocols"; + info->description = "test multi-protocol client"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_validate(test, (client = ast_websocket_client_create( + REMOTE_URL, "echo,unsupported", NULL, &result))); + + accept_protocol = ast_websocket_client_accept_protocol(client); + ast_test_validate(test, accept_protocol && !strcmp(accept_protocol, "echo")); + + return AST_TEST_PASS; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(websocket_client_create_and_connect); + AST_TEST_REGISTER(websocket_client_bad_url); + AST_TEST_REGISTER(websocket_client_unsupported_protocol); + AST_TEST_REGISTER(websocket_client_multiple_protocols); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(websocket_client_multiple_protocols); + AST_TEST_UNREGISTER(websocket_client_unsupported_protocol); + AST_TEST_UNREGISTER(websocket_client_bad_url); + AST_TEST_UNREGISTER(websocket_client_create_and_connect); + return 0; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Websocket client test module");