summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/asterisk/http.h59
-rw-r--r--include/asterisk/http_websocket.h90
-rw-r--r--include/asterisk/uri.h181
-rw-r--r--main/http.c107
-rw-r--r--main/uri.c321
-rw-r--r--res/res_http_websocket.c427
-rw-r--r--res/res_http_websocket.exports.in18
-rw-r--r--tests/test_uri.c154
-rw-r--r--tests/test_websocket_client.c165
9 files changed, 1487 insertions, 35 deletions
diff --git a/include/asterisk/http.h b/include/asterisk/http.h
index 0642cfa9b..35c8b22bd 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 10cb9a023..d95e6068e 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>
*
@@ -146,6 +154,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
*
* \param session Pointer to the WebSocket session
@@ -159,6 +181,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
*
* \param session Pointer to the WebSocket session
@@ -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 000000000..225d8c8d7
--- /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 783a34cfe..0c9395d9c 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 000000000..6642d843b
--- /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 cdb639f48..07cb6b7be 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 de3d02625..8177fc21b 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 000000000..92bbb70b0
--- /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 000000000..e104ed825
--- /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");