summaryrefslogtreecommitdiff
path: root/res/res_http_websocket.c
diff options
context:
space:
mode:
authorKevin Harwell <kharwell@digium.com>2014-06-05 17:22:35 +0000
committerKevin Harwell <kharwell@digium.com>2014-06-05 17:22:35 +0000
commite763d704700674342c8957f82bddeab3e15dfa08 (patch)
tree14ffdb60c71b6e5976783d97778ba485c4954e6b /res/res_http_websocket.c
parentfd45b822470264d50d80d419e65655ea01842da7 (diff)
res_http_websocket: Create a websocket client
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
Diffstat (limited to 'res/res_http_websocket.c')
-rw-r--r--res/res_http_websocket.c427
1 files changed, 410 insertions, 17 deletions
diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c
index cdb639f48..07cb6b7be 100644
--- a/res/res_http_websocket.c
+++ b/res/res_http_websocket.c
@@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
#include "asterisk/strings.h"
#include "asterisk/file.h"
#include "asterisk/unaligned.h"
+#include "asterisk/uri.h"
#define AST_API_MODULE
#include "asterisk/http_websocket.h"
@@ -44,6 +45,9 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
/*! \brief GUID used to compute the accept key, defined in the specifications */
#define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+/*! \brief Length of a websocket's client key */
+#define CLIENT_KEY_SIZE 16
+
/*! \brief Number of buckets for registered protocols */
#define MAX_PROTOCOL_BUCKETS 7
@@ -80,6 +84,7 @@ struct ast_websocket {
unsigned int secure:1; /*!< Bit to indicate that the transport is secure */
unsigned int closing:1; /*!< Bit to indicate that the session is in the process of being closed */
unsigned int close_sent:1; /*!< Bit to indicate that the session close opcode has been sent and no further data will be sent */
+ struct websocket_client *client; /*!< Client object when connected as a client websocket */
};
/*! \brief Structure definition for protocols */
@@ -165,13 +170,13 @@ static void session_destroy_fn(void *obj)
{
struct ast_websocket *session = obj;
- ast_websocket_close(session, 0);
-
if (session->f) {
+ ast_websocket_close(session, 0);
fclose(session->f);
ast_verb(2, "WebSocket connection from '%s' closed\n", ast_sockaddr_stringify(&session->address));
}
+ ao2_cleanup(session->client);
ast_free(session->payload);
}
@@ -578,6 +583,19 @@ static struct websocket_protocol *one_protocol(
return ao2_callback(server->protocols, OBJ_NOLOCK, NULL, NULL);
}
+static char *websocket_combine_key(const char *key, char *res, int res_size)
+{
+ char *combined;
+ unsigned combined_length = strlen(key) + strlen(WEBSOCKET_GUID) + 1;
+ uint8_t sha[20];
+
+ combined = ast_alloca(combined_length);
+ snprintf(combined, combined_length, "%s%s", key, WEBSOCKET_GUID);
+ ast_sha1_hash_uint(sha, combined);
+ ast_base64encode(res, (const unsigned char*)sha, 20, res_size);
+ return res;
+}
+
int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_vars, struct ast_variable *headers)
{
struct ast_variable *v;
@@ -662,15 +680,9 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan
/* Determine how to respond depending on the version */
if (version == 7 || version == 8 || version == 13) {
- /* Version 7 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 */
- /* Version 8 defined in specification http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 */
- /* Version 13 defined in specification http://tools.ietf.org/html/rfc6455 */
- char *combined, base64[64];
- unsigned combined_length;
- uint8_t sha[20];
-
- combined_length = (key ? strlen(key) : 0) + strlen(WEBSOCKET_GUID) + 1;
- if (!key || combined_length > 8192) { /* no stack overflows please */
+ char base64[64];
+
+ if (!key || strlen(key) + strlen(WEBSOCKET_GUID) + 1 > 8192) { /* no stack overflows please */
fputs("HTTP/1.1 400 Bad Request\r\n"
"Sec-WebSocket-Version: 7, 8, 13\r\n\r\n", ser->f);
ao2_ref(protocol_handler, -1);
@@ -686,17 +698,12 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan
return 0;
}
- combined = ast_alloca(combined_length);
- snprintf(combined, combined_length, "%s%s", key, WEBSOCKET_GUID);
- ast_sha1_hash_uint(sha, combined);
- ast_base64encode(base64, (const unsigned char*)sha, 20, sizeof(base64));
-
fprintf(ser->f, "HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: %s\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n",
upgrade,
- base64);
+ websocket_combine_key(key, base64, sizeof(base64)));
/* RFC 6455, Section 4.1:
*
@@ -844,6 +851,392 @@ int AST_OPTIONAL_API_NAME(ast_websocket_remove_protocol)(const char *name, ast_w
return res;
}
+/*! \brief Parse the given uri into a path and remote address.
+ *
+ * Expected uri form: [ws[s]]://<host>[:port][/<path>]
+ *
+ * The returned host will contain the address and optional port while
+ * path will contain everything after the address/port if included.
+ */
+static int websocket_client_parse_uri(const char *uri, char **host, char **path)
+{
+ struct ast_uri *parsed_uri = ast_uri_parse_websocket(uri);
+
+ if (!parsed_uri) {
+ return -1;
+ }
+
+ *host = ast_uri_make_host_with_port(parsed_uri);
+
+ if (ast_uri_path(parsed_uri) && !(*path = ast_strdup(ast_uri_path(parsed_uri)))) {
+ ao2_ref(parsed_uri, -1);
+ return -1;
+ }
+
+ ao2_ref(parsed_uri, -1);
+ return 0;
+}
+
+static void websocket_client_args_destroy(void *obj)
+{
+ struct ast_tcptls_session_args *args = obj;
+
+ if (args->tls_cfg) {
+ ast_free(args->tls_cfg->certfile);
+ ast_free(args->tls_cfg->pvtfile);
+ ast_free(args->tls_cfg->cipher);
+ ast_free(args->tls_cfg->cafile);
+ ast_free(args->tls_cfg->capath);
+
+ ast_ssl_teardown(args->tls_cfg);
+ }
+ ast_free(args->tls_cfg);
+}
+
+static struct ast_tcptls_session_args *websocket_client_args_create(
+ const char *host, struct ast_tls_config *tls_cfg,
+ enum ast_websocket_result *result)
+{
+ struct ast_sockaddr *addr;
+ struct ast_tcptls_session_args *args = ao2_alloc(
+ sizeof(*args), websocket_client_args_destroy);
+
+ if (!args) {
+ *result = WS_ALLOCATE_ERROR;
+ return NULL;
+ }
+
+ args->accept_fd = -1;
+ args->tls_cfg = tls_cfg;
+ args->name = "websocket client";
+
+ if (!ast_sockaddr_resolve(&addr, host, 0, 0)) {
+ ast_log(LOG_ERROR, "Unable to resolve address %s\n",
+ host);
+ ao2_ref(args, -1);
+ *result = WS_URI_RESOLVE_ERROR;
+ return NULL;
+ }
+ ast_sockaddr_copy(&args->remote_address, addr);
+ ast_free(addr);
+ return args;
+}
+
+static char *websocket_client_create_key(void)
+{
+ static int encoded_size = CLIENT_KEY_SIZE * 2 * sizeof(char) + 1;
+ /* key is randomly selected 16-byte base64 encoded value */
+ unsigned char key[CLIENT_KEY_SIZE + sizeof(long) - 1];
+ char *encoded = ast_malloc(encoded_size);
+ long i = 0;
+
+ if (!encoded) {
+ ast_log(LOG_ERROR, "Unable to allocate client websocket key\n");
+ return NULL;
+ }
+
+ while (i < CLIENT_KEY_SIZE) {
+ long num = ast_random();
+ memcpy(key + i, &num, sizeof(long));
+ i += sizeof(long);
+ }
+
+ ast_base64encode(encoded, key, CLIENT_KEY_SIZE, encoded_size);
+ return encoded;
+}
+
+struct websocket_client {
+ /*! host portion of client uri */
+ char *host;
+ /*! path for logical websocket connection */
+ char *resource_name;
+ /*! unique key used during server handshaking */
+ char *key;
+ /*! container for registered protocols */
+ char *protocols;
+ /*! the protocol accepted by the server */
+ char *accept_protocol;
+ /*! websocket protocol version */
+ int version;
+ /*! tcptls connection arguments */
+ struct ast_tcptls_session_args *args;
+ /*! tcptls connection instance */
+ struct ast_tcptls_session_instance *ser;
+};
+
+static void websocket_client_destroy(void *obj)
+{
+ struct websocket_client *client = obj;
+
+ ao2_cleanup(client->ser);
+ ao2_cleanup(client->args);
+
+ ast_free(client->accept_protocol);
+ ast_free(client->protocols);
+ ast_free(client->key);
+ ast_free(client->resource_name);
+ ast_free(client->host);
+}
+
+static struct ast_websocket * websocket_client_create(
+ const char *uri, const char *protocols, struct ast_tls_config *tls_cfg,
+ enum ast_websocket_result *result)
+{
+ struct ast_websocket *ws = ao2_alloc(sizeof(*ws), session_destroy_fn);
+
+ if (!ws) {
+ ast_log(LOG_ERROR, "Unable to allocate websocket\n");
+ *result = WS_ALLOCATE_ERROR;
+ return NULL;
+ }
+
+ if (!(ws->client = ao2_alloc(
+ sizeof(*ws->client), websocket_client_destroy))) {
+ ast_log(LOG_ERROR, "Unable to allocate websocket client\n");
+ *result = WS_ALLOCATE_ERROR;
+ return NULL;
+ }
+
+ if (!(ws->client->key = websocket_client_create_key())) {
+ ao2_ref(ws, -1);
+ *result = WS_KEY_ERROR;
+ return NULL;
+ }
+
+ if (websocket_client_parse_uri(
+ uri, &ws->client->host, &ws->client->resource_name)) {
+ ao2_ref(ws, -1);
+ *result = WS_URI_PARSE_ERROR;
+ return NULL;
+ }
+
+ if (!(ws->client->args = websocket_client_args_create(
+ ws->client->host, tls_cfg, result))) {
+ ao2_ref(ws, -1);
+ return NULL;
+ }
+ ws->client->protocols = ast_strdup(protocols);
+
+ ws->client->version = 13;
+ ws->opcode = -1;
+ ws->reconstruct = DEFAULT_RECONSTRUCTION_CEILING;
+ return ws;
+}
+
+const char * AST_OPTIONAL_API_NAME(
+ ast_websocket_client_accept_protocol)(struct ast_websocket *ws)
+{
+ return ws->client->accept_protocol;
+}
+
+static enum ast_websocket_result websocket_client_handle_response_code(
+ struct websocket_client *client, int response_code)
+{
+ if (response_code <= 0) {
+ return WS_INVALID_RESPONSE;
+ }
+
+ switch (response_code) {
+ case 101:
+ return 0;
+ case 400:
+ ast_log(LOG_ERROR, "Received response 400 - Bad Request "
+ "- from %s\n", client->host);
+ return WS_BAD_REQUEST;
+ case 404:
+ ast_log(LOG_ERROR, "Received response 404 - Request URL not "
+ "found - from %s\n", client->host);
+ return WS_URL_NOT_FOUND;
+ }
+
+ ast_log(LOG_ERROR, "Invalid HTTP response code %d from %s\n",
+ response_code, client->host);
+ return WS_INVALID_RESPONSE;
+}
+
+static enum ast_websocket_result websocket_client_handshake_get_response(
+ struct websocket_client *client)
+{
+ enum ast_websocket_result res;
+ char buf[4096];
+ char base64[64];
+ int has_upgrade = 0;
+ int has_connection = 0;
+ int has_accept = 0;
+ int has_protocol = 0;
+
+ if (!fgets(buf, sizeof(buf), client->ser->f)) {
+ ast_log(LOG_ERROR, "Unable to retrieve HTTP status line.");
+ return WS_BAD_STATUS;
+ }
+
+ if ((res = websocket_client_handle_response_code(client,
+ ast_http_response_status_line(
+ buf, "HTTP/1.1", 101))) != WS_OK) {
+ return res;
+ }
+
+ /* Ignoring line folding - assuming header field values are contained
+ within a single line */
+ while (fgets(buf, sizeof(buf), client->ser->f)) {
+ char *name, *value;
+ int parsed = ast_http_header_parse(buf, &name, &value);
+
+ if (parsed < 0) {
+ break;
+ }
+
+ if (parsed > 0) {
+ continue;
+ }
+
+ if (!has_upgrade &&
+ (has_upgrade = ast_http_header_match(
+ name, "upgrade", value, "websocket")) < 0) {
+ return WS_HEADER_MISMATCH;
+ } else if (!has_connection &&
+ (has_connection = ast_http_header_match(
+ name, "connection", value, "upgrade")) < 0) {
+ return WS_HEADER_MISMATCH;
+ } else if (!has_accept &&
+ (has_accept = ast_http_header_match(
+ name, "sec-websocket-accept", value,
+ websocket_combine_key(
+ client->key, base64, sizeof(base64)))) < 0) {
+ return WS_HEADER_MISMATCH;
+ } else if (!has_protocol &&
+ (has_protocol = ast_http_header_match_in(
+ name, "sec-websocket-protocol", value, client->protocols))) {
+ if (has_protocol < 0) {
+ return WS_HEADER_MISMATCH;
+ }
+ client->accept_protocol = ast_strdup(value);
+ } else if (!strcasecmp(name, "sec-websocket-extensions")) {
+ ast_log(LOG_ERROR, "Extensions received, but not "
+ "supported by client\n");
+ return WS_NOT_SUPPORTED;
+ }
+ }
+ return has_upgrade && has_connection && has_accept ?
+ WS_OK : WS_HEADER_MISSING;
+}
+
+static enum ast_websocket_result websocket_client_handshake(
+ struct websocket_client *client)
+{
+ char protocols[100] = "";
+
+ if (!ast_strlen_zero(client->protocols)) {
+ sprintf(protocols, "Sec-WebSocket-Protocol: %s\r\n",
+ client->protocols);
+ }
+
+ if (fprintf(client->ser->f,
+ "GET /%s HTTP/1.1\r\n"
+ "Sec-WebSocket-Version: %d\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Host: %s\r\n"
+ "Sec-WebSocket-Key: %s\r\n"
+ "%s\r\n",
+ client->resource_name,
+ client->version,
+ client->host,
+ client->key,
+ protocols) < 0) {
+ ast_log(LOG_ERROR, "Failed to send handshake.\n");
+ return WS_WRITE_ERROR;
+ }
+ /* wait for a response before doing anything else */
+ return websocket_client_handshake_get_response(client);
+}
+
+static enum ast_websocket_result websocket_client_connect(struct ast_websocket *ws)
+{
+ enum ast_websocket_result res;
+ /* create and connect the client - note client_start
+ releases the session instance on failure */
+ if (!(ws->client->ser = ast_tcptls_client_start(
+ ast_tcptls_client_create(ws->client->args)))) {
+ return WS_CLIENT_START_ERROR;
+ }
+
+ if ((res = websocket_client_handshake(ws->client)) != WS_OK) {
+ ao2_ref(ws->client->ser, -1);
+ ws->client->ser = NULL;
+ return res;
+ }
+
+ ws->f = ws->client->ser->f;
+ ws->fd = ws->client->ser->fd;
+ ws->secure = ws->client->ser->ssl ? 1 : 0;
+ ast_sockaddr_copy(&ws->address, &ws->client->ser->remote_address);
+ return WS_OK;
+}
+
+struct ast_websocket *AST_OPTIONAL_API_NAME(ast_websocket_client_create)
+ (const char *uri, const char *protocols, struct ast_tls_config *tls_cfg,
+ enum ast_websocket_result *result)
+{
+ struct ast_websocket *ws = websocket_client_create(
+ uri, protocols, tls_cfg, result);
+
+ if (!ws) {
+ return NULL;
+ }
+
+ if ((*result = websocket_client_connect(ws)) != WS_OK) {
+ ao2_ref(ws, -1);
+ return NULL;
+ }
+
+ return ws;
+}
+
+int AST_OPTIONAL_API_NAME(ast_websocket_read_string)
+ (struct ast_websocket *ws, struct ast_str **buf)
+{
+ char *payload;
+ uint64_t payload_len;
+ enum ast_websocket_opcode opcode;
+ int fragmented = 1;
+
+ if (!*buf && !(*buf = ast_str_create(512))) {
+ ast_log(LOG_ERROR, "Client Websocket string read - "
+ "Unable to allocate string buffer");
+ return -1;
+ }
+
+ while (fragmented) {
+ if (ast_websocket_read(ws, &payload, &payload_len,
+ &opcode, &fragmented)) {
+ ast_log(LOG_ERROR, "Client WebSocket string read - "
+ "error reading string data\n");
+ return -1;
+ }
+
+ if (opcode == AST_WEBSOCKET_OPCODE_CLOSE) {
+ return -1;
+ }
+
+ if (opcode != AST_WEBSOCKET_OPCODE_TEXT) {
+ ast_log(LOG_ERROR, "Client WebSocket string read - "
+ "non string data received\n");
+ return -1;
+ }
+
+ ast_str_append(buf, 0, "%s", payload);
+ }
+ return ast_str_size(*buf);
+}
+
+int AST_OPTIONAL_API_NAME(ast_websocket_write_string)
+ (struct ast_websocket *ws, const struct ast_str *buf)
+{
+ return ast_websocket_write(ws, AST_WEBSOCKET_OPCODE_TEXT,
+ ast_str_buffer(buf), ast_str_strlen(buf));
+}
+
static int load_module(void)
{
websocketuri.data = websocket_server_internal_create();