res_http_websocket: Create a websocket client
authorKevin Harwell <kharwell@digium.com>
Thu, 5 Jun 2014 17:22:35 +0000 (17:22 +0000)
committerKevin Harwell <kharwell@digium.com>
Thu, 5 Jun 2014 17:22:35 +0000 (17:22 +0000)
Added a websocket server client in Asterisk. Asterisk has a websocket server,
but not a client. The ability to have Asterisk be able to connect to a websocket
server can potentially be useful for future work (for instance this could allow
ARI to connect back to some external system, although more work would be needed
in order to incorporate that).

Also a couple of things to note - proxy connection support has not been
implemented and there is limited http response code handling (basically, it is
connect or not).

Also added an initial new URI handling mechanism to core.  Internet type URI's
are parsed into a data structure that contains pointers to the various parts of
the URI.

(closes issue ASTERISK-23742)
Reported by: Kevin Harwell
Review: https://reviewboard.asterisk.org/r/3541/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@415223 65c4cc65-6c06-0410-ace0-fbb531ad65f3

include/asterisk/http.h
include/asterisk/http_websocket.h
include/asterisk/uri.h [new file with mode: 0644]
main/http.c
main/uri.c [new file with mode: 0644]
res/res_http_websocket.c
res/res_http_websocket.exports.in
tests/test_uri.c [new file with mode: 0644]
tests/test_websocket_client.c [new file with mode: 0644]

index 0642cfa..35c8b22 100644 (file)
@@ -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 */
index 10cb9a0..d95e606 100644 (file)
 
 /*!
  * \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 (file)
index 0000000..225d8c8
--- /dev/null
@@ -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 */
index 783a34c..0c9395d 100644 (file)
@@ -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 (file)
index 0000000..6642d84
--- /dev/null
@@ -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;
+}
index cdb639f..07cb6b7 100644 (file)
@@ -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();
index de3d026..8177fc2 100644 (file)
@@ -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 (file)
index 0000000..92bbb70
--- /dev/null
@@ -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 (file)
index 0000000..e104ed8
--- /dev/null
@@ -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");