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