From 31cc24aad603716e7ef68b384faebd14e0dfa460 Mon Sep 17 00:00:00 2001 From: Matt Jordan Date: Wed, 13 May 2015 09:55:58 -0500 Subject: res/res_http_websocket: Add a pre-session established callback This patch updates http_websocket and its corresponding implementation with a pre-session established callback. This callback allows for WebSocket server consumers to be notified when a WebSocket connection is attempted, but before we accept it. Consumers can choose to reject the connection, if their application specific logic allows for it. As a result, this patch pulls out the previously private websocket_protocol struct and makes it public, as ast_websocket_protocol. In order to preserve backwards compatibility with existing modules, the existing APIs were left as-is, and new APIs were added for the creation of the ast_websocket_protocol as well as for adding a sub-protocol to a WebSocket server. In particular, the following new API calls were added: * ast_websocket_add_protocol2 - add a protocol to the core WebSocket server * ast_websocket_server_add_protocol2 - add a protocol to a specific WebSocket server * ast_websocket_sub_protocol_alloc - allocate a sub-protocol object. Consumers can populate this with whatever callbacks they wish to support, then add it to the core server or a specified server. ASTERISK-24988 Reported by: Joshua Colp Change-Id: Ibe0bbb30c17eec6b578071bdbd197c911b620ab2 --- include/asterisk/http_websocket.h | 86 +++++++++++++++++++++++++++++- res/res_http_websocket.c | 109 ++++++++++++++++++++++++++++---------- 2 files changed, 167 insertions(+), 28 deletions(-) diff --git a/include/asterisk/http_websocket.h b/include/asterisk/http_websocket.h index 3e07e608b..5adc08925 100644 --- a/include/asterisk/http_websocket.h +++ b/include/asterisk/http_websocket.h @@ -67,6 +67,24 @@ struct ast_websocket_server; */ struct ast_websocket; +/*! + * \brief Callback from the HTTP request attempting to establish a websocket connection + * + * This callback occurs when an HTTP request is made to establish a websocket connection. + * Implementers of \ref ast_websocket_protocol can use this to deny a request, or to + * set up application specific data before invocation of \ref ast_websocket_callback. + * + * \param ser The TCP/TLS session + * \param parameters Parameters extracted from the request URI + * \param headers Headers included in the request + * + * \retval 0 The session should be accepted + * \retval -1 The session should be rejected. Note that the caller must send an error + * response using \ref ast_http_error. + * \since 13.5.0 + */ +typedef int (*ast_websocket_pre_callback)(struct ast_tcptls_session_instance *ser, struct ast_variable *parameters, struct ast_variable *headers); + /*! * \brief Callback for when a new connection for a sub-protocol is established * @@ -80,6 +98,32 @@ struct ast_websocket; */ typedef void (*ast_websocket_callback)(struct ast_websocket *session, struct ast_variable *parameters, struct ast_variable *headers); +/*! + * \brief A websocket protocol implementation + * + * Users of the Websocket API can register themselves as a websocket + * protocol. See \ref ast_websocket_add_protocol2 and \ref ast_websocket_server_add_protocol2. + * Simpler implementations may use only \ref ast_websocket_add_protocol and + * \ref ast_websocket_server_add_protocol. + * + * \since 13.5.0 + */ +struct ast_websocket_protocol { + /*! \brief Name of the protocol */ + char *name; +/*! + * \brief Protocol version. This prevents dynamically loadable modules from registering + * if this struct is changed. + */ +#define AST_WEBSOCKET_PROTOCOL_VERSION 1 + /*! \brief Protocol version. Should be set to /ref AST_WEBSOCKET_PROTOCOL_VERSION */ + unsigned int version; + /*! \brief Callback called when a new session is attempted. Optional. */ + ast_websocket_pre_callback session_attempted; + /* \brief Callback called when a new session is established. Mandatory. */ + ast_websocket_callback session_established; +}; + /*! * \brief Creates a \ref websocket_server * @@ -97,6 +141,15 @@ AST_OPTIONAL_API(struct ast_websocket_server *, ast_websocket_server_create, (vo */ AST_OPTIONAL_API(int, 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), { return -1; }); +/*! + * \brief Allocate a websocket sub-protocol instance + * + * \retval An instance of \ref ast_websocket_protocol on success + * \retval NULL on error + * \since 13.5.0 + */ +AST_OPTIONAL_API(struct ast_websocket_protocol *, ast_websocket_sub_protocol_alloc, (const char *name), {return NULL;}); + /*! * \brief Add a sub-protocol handler to the default /ws server * @@ -108,11 +161,26 @@ AST_OPTIONAL_API(int, ast_websocket_uri_cb, (struct ast_tcptls_session_instance */ AST_OPTIONAL_API(int, ast_websocket_add_protocol, (const char *name, ast_websocket_callback callback), {return -1;}); +/*! + * \brief Add a sub-protocol handler to the default /ws server + * + * \param protocol The sub-protocol to register. Note that this must + * be allocated using /ref ast_websocket_sub_protocol_alloc. + * + * \note This method is reference stealing. It will steal the reference to \ref protocol + * on success. + * + * \retval 0 success + * \retval -1 if sub-protocol handler could not be registered + * \since 13.5.0 + */ +AST_OPTIONAL_API(int, ast_websocket_add_protocol2, (struct ast_websocket_protocol *protocol), {return -1;}); + /*! * \brief Remove a sub-protocol handler from the default /ws server. * * \param name Name of the sub-protocol to unregister - * \param callback Callback that was previously registered with the sub-protocol + * \param callback Session Established callback that was previously registered with the sub-protocol * * \retval 0 success * \retval -1 if sub-protocol was not found or if callback did not match @@ -131,6 +199,22 @@ AST_OPTIONAL_API(int, ast_websocket_remove_protocol, (const char *name, ast_webs */ AST_OPTIONAL_API(int, ast_websocket_server_add_protocol, (struct ast_websocket_server *server, const char *name, ast_websocket_callback callback), {return -1;}); +/*! + * \brief Add a sub-protocol handler to the given server. + * + * \param server The server to add the sub-protocol to. + * \param protocol The sub-protocol to register. Note that this must + * be allocated using /ref ast_websocket_sub_protocol_alloc. + * + * \note This method is reference stealing. It will steal the reference to \ref protocol + * on success. + * + * \retval 0 success + * \retval -1 if sub-protocol handler could not be registered + * \since 13.5.0 + */ +AST_OPTIONAL_API(int, ast_websocket_server_add_protocol2, (struct ast_websocket_server *server, struct ast_websocket_protocol *protocol), {return -1;}); + /*! * \brief Remove a sub-protocol handler from the given server. * diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c index 7e71f8bf1..9e8b680a9 100644 --- a/res/res_http_websocket.c +++ b/res/res_http_websocket.c @@ -88,16 +88,10 @@ struct ast_websocket { struct websocket_client *client; /*!< Client object when connected as a client websocket */ }; -/*! \brief Structure definition for protocols */ -struct websocket_protocol { - char *name; /*!< Name of the protocol */ - ast_websocket_callback callback; /*!< Callback called when a new session is established */ -}; - /*! \brief Hashing function for protocols */ static int protocol_hash_fn(const void *obj, const int flags) { - const struct websocket_protocol *protocol = obj; + const struct ast_websocket_protocol *protocol = obj; const char *name = obj; return ast_str_case_hash(flags & OBJ_KEY ? name : protocol->name); @@ -106,7 +100,7 @@ static int protocol_hash_fn(const void *obj, const int flags) /*! \brief Comparison function for protocols */ static int protocol_cmp_fn(void *obj, void *arg, int flags) { - const struct websocket_protocol *protocol1 = obj, *protocol2 = arg; + const struct ast_websocket_protocol *protocol1 = obj, *protocol2 = arg; const char *protocol = arg; return !strcasecmp(protocol1->name, flags & OBJ_KEY ? protocol : protocol2->name) ? CMP_MATCH | CMP_STOP : 0; @@ -115,7 +109,7 @@ static int protocol_cmp_fn(void *obj, void *arg, int flags) /*! \brief Destructor function for protocols */ static void protocol_destroy_fn(void *obj) { - struct websocket_protocol *protocol = obj; + struct ast_websocket_protocol *protocol = obj; ast_free(protocol->name); } @@ -182,54 +176,91 @@ static void session_destroy_fn(void *obj) ast_free(session->payload); } +struct ast_websocket_protocol *AST_OPTIONAL_API_NAME(ast_websocket_sub_protocol_alloc)(const char *name) +{ + struct ast_websocket_protocol *protocol; + + protocol = ao2_alloc(sizeof(*protocol), protocol_destroy_fn); + if (!protocol) { + return NULL; + } + + protocol->name = ast_strdup(name); + if (!protocol->name) { + ao2_ref(protocol, -1); + return NULL; + } + protocol->version = AST_WEBSOCKET_PROTOCOL_VERSION; + + return protocol; +} + int AST_OPTIONAL_API_NAME(ast_websocket_server_add_protocol)(struct ast_websocket_server *server, const char *name, ast_websocket_callback callback) { - struct websocket_protocol *protocol; + struct ast_websocket_protocol *protocol; if (!server->protocols) { return -1; } - ao2_lock(server->protocols); + protocol = ast_websocket_sub_protocol_alloc(name); + if (!protocol) { + ao2_unlock(server->protocols); + return -1; + } + protocol->session_established = callback; - /* Ensure a second protocol handler is not registered for the same protocol */ - if ((protocol = ao2_find(server->protocols, name, OBJ_KEY | OBJ_NOLOCK))) { + if (ast_websocket_server_add_protocol2(server, protocol)) { ao2_ref(protocol, -1); - ao2_unlock(server->protocols); return -1; } - if (!(protocol = ao2_alloc(sizeof(*protocol), protocol_destroy_fn))) { - ao2_unlock(server->protocols); + return 0; +} + +int AST_OPTIONAL_API_NAME(ast_websocket_server_add_protocol2)(struct ast_websocket_server *server, struct ast_websocket_protocol *protocol) +{ + struct ast_websocket_protocol *existing; + + if (!server->protocols) { return -1; } - if (!(protocol->name = ast_strdup(name))) { - ao2_ref(protocol, -1); - ao2_unlock(server->protocols); + if (protocol->version != AST_WEBSOCKET_PROTOCOL_VERSION) { + ast_log(LOG_WARNING, "WebSocket could not register sub-protocol '%s': " + "expected version '%u', got version '%u'\n", + protocol->name, AST_WEBSOCKET_PROTOCOL_VERSION, protocol->version); return -1; } - protocol->callback = callback; + ao2_lock(server->protocols); + + /* Ensure a second protocol handler is not registered for the same protocol */ + existing = ao2_find(server->protocols, protocol->name, OBJ_KEY | OBJ_NOLOCK); + if (existing) { + ao2_ref(existing, -1); + ao2_unlock(server->protocols); + return -1; + } ao2_link_flags(server->protocols, protocol, OBJ_NOLOCK); ao2_unlock(server->protocols); - ao2_ref(protocol, -1); - ast_verb(2, "WebSocket registered sub-protocol '%s'\n", name); + ast_verb(2, "WebSocket registered sub-protocol '%s'\n", protocol->name); + ao2_ref(protocol, -1); return 0; } int AST_OPTIONAL_API_NAME(ast_websocket_server_remove_protocol)(struct ast_websocket_server *server, const char *name, ast_websocket_callback callback) { - struct websocket_protocol *protocol; + struct ast_websocket_protocol *protocol; if (!(protocol = ao2_find(server->protocols, name, OBJ_KEY))) { return -1; } - if (protocol->callback != callback) { + if (protocol->session_established != callback) { ao2_ref(protocol, -1); return -1; } @@ -600,7 +631,7 @@ int AST_OPTIONAL_API_NAME(ast_websocket_read)(struct ast_websocket *session, cha /*! * \brief If the server has exactly one configured protocol, return it. */ -static struct websocket_protocol *one_protocol( +static struct ast_websocket_protocol *one_protocol( struct ast_websocket_server *server) { SCOPED_AO2LOCK(lock, server->protocols); @@ -643,7 +674,7 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan struct ast_variable *v; char *upgrade = NULL, *key = NULL, *key1 = NULL, *key2 = NULL, *protos = NULL, *requested_protocols = NULL, *protocol = NULL; int version = 0, flags = 1; - struct websocket_protocol *protocol_handler = NULL; + struct ast_websocket_protocol *protocol_handler = NULL; struct ast_websocket *session; struct ast_websocket_server *server; @@ -742,6 +773,14 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan } session->timeout = AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT; + if (protocol_handler->session_attempted + && protocol_handler->session_attempted(ser, get_vars, headers)) { + ast_debug(3, "WebSocket connection from '%s' rejected by protocol handler '%s'\n", + ast_sockaddr_stringify(&ser->remote_address), protocol_handler->name); + ao2_ref(protocol_handler, -1); + return 0; + } + fprintf(ser->f, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: %s\r\n" "Connection: Upgrade\r\n" @@ -797,7 +836,7 @@ int AST_OPTIONAL_API_NAME(ast_websocket_uri_cb)(struct ast_tcptls_session_instan /* Give up ownership of the socket and pass it to the protocol handler */ ast_tcptls_stream_set_exclusive_input(ser->stream_cookie, 0); - protocol_handler->callback(session, get_vars, headers); + protocol_handler->session_established(session, get_vars, headers); ao2_ref(protocol_handler, -1); /* @@ -881,6 +920,22 @@ int AST_OPTIONAL_API_NAME(ast_websocket_add_protocol)(const char *name, ast_webs return res; } +int AST_OPTIONAL_API_NAME(ast_websocket_add_protocol2)(struct ast_websocket_protocol *protocol) +{ + struct ast_websocket_server *ws_server = websocketuri.data; + + if (!ws_server) { + return -1; + } + + if (ast_websocket_server_add_protocol2(ws_server, protocol)) { + return -1; + } + + ast_module_ref(ast_module_info->self); + return 0; +} + static int websocket_remove_protocol_internal(const char *name, ast_websocket_callback callback) { struct ast_websocket_server *ws_server = websocketuri.data; -- cgit v1.2.3