diff --git a/include/asterisk/http.h b/include/asterisk/http.h index 033ae2debbf..2940d9d4ff8 100644 --- a/include/asterisk/http.h +++ b/include/asterisk/http.h @@ -352,4 +352,51 @@ int ast_http_header_match(const char *name, const char *expected_name, int ast_http_header_match_in(const char *name, const char *expected_name, const char *value, const char *expected_value); +#ifdef TEST_FRAMEWORK + +/*! + * Currently multiple HTTP servers are only allowed when the TEST_FRAMEWORK + * is enabled, so putting this here: + * + * If a server is listening on 'any' (i.e. 0.0.0.0), and another server attempts + * to listen on 'localhost' on the same port (and vice versa) then you'll get an + * "Address already in use" error. For now use a different port, or match the + * addresses exactly. + */ + +struct ast_http_server; + +/*! + * \brief Retrieve a HTTP server listening at the given host + * + * A given host can include the port, e.g. [:]. If no port is specified + * then the port defaults to '8088'. If a host parameter is NULL, or empty and a + * configured server is already listening then that server is returned. If no + * server is and enabled then the host defaults to 'localhost:8088'. + * + * \note When finished with a successfully returned server object + * ast_http_test_server_discard MUST be called on the object + * in order for proper 'cleanup' to occur. + * + * \param name Optional name for the server (default 'http test server') + * \param host Optional host, or address with port to bind to (default 'localhost:8088') + * + * \return a HTTP server object, or NULL on error + */ +struct ast_http_server *ast_http_test_server_get(const char *name, const char *host); + +/*! + * \brief Discard, or drop a HTTP server + * + * This function MUST eventually be called for every successful call to + * ast_http_test_server_get. + * + * \note NULL tolerant + * + * \param server The HTTP server to discard + */ +void ast_http_test_server_discard(struct ast_http_server *server); + +#endif + #endif /* _ASTERISK_SRV_H */ diff --git a/main/http.c b/main/http.c index 78ac8f8894b..7f1ffe0c570 100644 --- a/main/http.c +++ b/main/http.c @@ -113,18 +113,21 @@ static struct ast_tls_config http_tls_cfg; static void *httpd_helper_thread(void *arg); /*! - * we have up to two accepting threads, one for http, one for https + * For standard configuration we have up to two accepting threads, + * one for http, one for https. If TEST_FRAMEWORK is enabled it's + * possible to have more than one running http server. */ -static struct ast_tcptls_session_args http_desc = { - .accept_fd = -1, - .master = AST_PTHREADT_NULL, - .tls_cfg = NULL, - .poll_timeout = -1, - .name = "http server", - .accept_fn = ast_tcptls_server_root, - .worker_fn = httpd_helper_thread, +struct ast_http_server { + struct ast_tcptls_session_args args; + char *name; + char *address; }; +/*! + * The default configured HTTP server + */ +struct ast_http_server *global_http_server = NULL; + static struct ast_tcptls_session_args https_desc = { .accept_fd = -1, .master = AST_PTHREADT_NULL, @@ -394,9 +397,9 @@ static int httpstatus_callback(struct ast_tcptls_session_instance *ser, ast_str_append(&out, 0, "Server%s\r\n", http_server_name); ast_str_append(&out, 0, "Prefix%s\r\n", prefix); ast_str_append(&out, 0, "Bind Address%s\r\n", - ast_sockaddr_stringify_addr(&http_desc.old_address)); + ast_sockaddr_stringify_addr(&global_http_server->args.old_address)); ast_str_append(&out, 0, "Bind Port%s\r\n", - ast_sockaddr_stringify_port(&http_desc.old_address)); + ast_sockaddr_stringify_port(&global_http_server->args.old_address)); if (http_tls_cfg.enabled) { ast_str_append(&out, 0, "SSL Bind Port%s\r\n", ast_sockaddr_stringify_port(&https_desc.old_address)); @@ -2074,6 +2077,293 @@ static void add_redirect(const char *value) AST_RWLIST_UNLOCK(&uri_redirects); } +/*! \brief Number of HTTP server buckets */ +#define HTTP_SERVER_BUCKETS 5 + +struct ao2_container *http_servers = NULL; + +AO2_STRING_FIELD_HASH_FN(ast_http_server, address); +AO2_STRING_FIELD_CMP_FN(ast_http_server, address); + +static void http_server_destroy(void *obj) +{ + struct ast_http_server *server = obj; + + ast_tcptls_server_stop(&server->args); + + ast_verb(1, "Stopped http server '%s' listening at '%s'\n", server->name, server->address); + + ast_free(server->name); + ast_free(server->address); +} + +static struct ast_http_server *http_server_create(const char *name, const char *address, + const struct ast_sockaddr *addr) +{ + struct ast_http_server *server; + + server = ao2_alloc(sizeof(*server), http_server_destroy); + if (!server) { + ast_log(LOG_ERROR, "Unable to allocate HTTP server '%s' at address '%s'\n", + name, address); + return NULL; + } + + if (!(server->address = ast_strdup(address)) || !(server->name = ast_strdup(name))) { + ast_log(LOG_ERROR, "Unable to complete setup for HTTP server '%s' at address '%s'\n", + name, address); + ao2_ref(server, -1); + return NULL; + } + + server->args.accept_fd = -1; + server->args.master = AST_PTHREADT_NULL; + server->args.tls_cfg = NULL; + server->args.poll_timeout = -1; + server->args.name = server->name; + server->args.accept_fn = ast_tcptls_server_root; + server->args.worker_fn = httpd_helper_thread; + + ast_sockaddr_copy(&server->args.local_address, addr); + + return server; +} + +static int http_server_start(struct ast_http_server *server) +{ + if (server->args.accept_fd != -1) { + /* Already running */ + return 0; + } + + ast_tcptls_server_start(&server->args); + if (server->args.accept_fd == -1) { + ast_log(LOG_WARNING, "Failed to start HTTP server '%s' at address '%s'\n", + server->name, server->address); + return -1; + } + + if (!ao2_link_flags(http_servers, server, OBJ_NOLOCK)) { + ast_log(LOG_WARNING, "Failed to link HTTP server '%s' at address '%s'\n", + server->name, server->address); + return -1; + } + + ast_verb(1, "Bound HTTP server '%s' to address %s\n", server->name, server->address); + + return 0; +} + +/*! + * \brief Discard/Drop a HTTP server + * + * Decrements the reference to the given object, and unlinks it from the servers + * container if it's the last reference. + * + * After a server object has been added to the container this method should always + * be called to decrement the object's reference instead of the regular ao2 methods. + * + * \note NULL tolerant + * + * \param server The server object + */ +static void http_server_discard(struct ast_http_server *server) +{ + if (!server) { + return; + } + + /* + * If only two references were on the object then the last one is from + * the servers container, so remove from container now. + */ + if (ao2_ref(server, -1) == 2) { + ao2_unlink(http_servers, server); + } +} + +/*! + * \brief Retrieve, or create a HTTP server object by sock address + * + * Look for, and return a matching server object by addr. If an object is not found + * then create a new one. + * + * \note This method should be called with the http_servers container already locked. + * + * \param name The name of the server + * \param addr The address to match on, or create a new object with + * + * \return a HTTP server object, or NULL on error + */ +static struct ast_http_server *http_server_get_by_addr( + const char *name, const struct ast_sockaddr *addr) +{ + struct ast_http_server *server; + const char *address; + + address = ast_sockaddr_stringify(addr); + if (ast_strlen_zero(address)) { + return NULL; + } + + server = ao2_find(http_servers, address, OBJ_KEY | OBJ_NOLOCK); + + return server ?: http_server_create(name, address, addr); +} + +/*! + * \brief Retrieve, or create a HTTP server object by host + * + * Resolve the given host, and then look for, and return a matching server object. + * If an object is not found then create a new one. + * + * \note This method should be called with the http_servers container already locked. + * + * \param name The name of the server + * \param host The host to resolve, and match on or create a new object with + * \param port Optional port used if one is not specified with the host (default 8088) + * + * \return a HTTP server object, or NULL on error + */ +static struct ast_http_server *http_server_get_by_host(const char *name, const char *host, + uint32_t port) +{ + struct ast_sockaddr *addrs = NULL; + int num_addrs; + int i; + + if (!(num_addrs = ast_sockaddr_resolve(&addrs, host, 0, AST_AF_UNSPEC))) { + ast_log(LOG_WARNING, "Unable to resolve host '%s'\n", host); + return NULL; + } + + if (port == 0) { + port = DEFAULT_PORT; + } + + for (i = 0; i < num_addrs; ++i) { + struct ast_http_server *server; + + /* Use the given port if one was not specified already */ + if (!ast_sockaddr_port(&addrs[i])) { + ast_sockaddr_set_port(&addrs[i], port); + } + + server = http_server_get_by_addr(name, &addrs[i]); + if (server) { + ast_free(addrs); + return server; + } + } + + ast_free(addrs); + return NULL; +} + +/*! + * \brief Retrieve, or create and start a HTTP server + * + * Resolve the given host, and retrieve a listening server object. If the server is + * not already listening then start it. If a replace_me parameter is given, and it + * points to a non-NULL value then that server is discarded and replaced. + * + * \param name The name of the server + * \param host The host to resolve, and match on or create a new object with + * \param port Optional port used if one is not specified with the host (default 8088) + * \param[out] replace_me Optional server to be replaced + * + * \note If replace_me is specified the returned value is always the same as what's + * passed back out in the variable. + * + * \return a HTTP server object, or NULL on error + */ +static struct ast_http_server *http_server_get(const char *name, const char *host, + uint32_t port, struct ast_http_server **replace_me) +{ + struct ast_http_server *server; + + ao2_lock(http_servers); + + server = http_server_get_by_host(name, host, port); + + if (replace_me) { + /* Only replace if different */ + if (*replace_me == server) { + ao2_cleanup(server); + ao2_unlock(http_servers); + return *replace_me; + } + + if (*replace_me) { + http_server_discard(*replace_me); + } + + *replace_me = server; + } + + if (server && http_server_start(server)) { + if (replace_me) { + *replace_me = NULL; + } + + ao2_ref(server, -1); + server = NULL; + } + + ao2_unlock(http_servers); + return server; +} + +#ifdef TEST_FRAMEWORK + +struct ast_http_server *ast_http_test_server_get(const char *name, const char *host) +{ + struct ast_http_server *server; + + /* + * Currently multiple HTTP servers are only allowed when the TEST_FRAMEWORK + * is enabled, leaving the follow 'todos' if they become a problem or if this + * ability moves outside the TEST_FRAMEWORK. + * + * TODO: Add locking around global_http_server use. If this module is reloading + * it's possible for the global_http_server to exist here, and then become + * NULL between the check and return. + * + * TODO: Make it so 'localhost' and 'any' addresses equate. + */ + + if (ast_strlen_zero(host)) { + /* Use configured server if one available */ + if (global_http_server) { + ast_module_ref(ast_module_info->self); + return ao2_bump(global_http_server); + } + + host = "localhost:8088"; + } + + if (!name) { + name = "http test server"; + } + + server = http_server_get(name, host, 0, NULL); + if (server) { + ast_module_ref(ast_module_info->self); + } + + return server; +} + +void ast_http_test_server_discard(struct ast_http_server *server) +{ + if (server) { + http_server_discard(server); + ast_module_unref(ast_module_info->self); + } +} + +#endif + static int __ast_http_load(int reload) { struct ast_config *cfg; @@ -2086,9 +2376,8 @@ static int __ast_http_load(int reload) struct http_uri_redirect *redirect; struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; uint32_t bindport = DEFAULT_PORT; - RAII_VAR(struct ast_sockaddr *, addrs, NULL, ast_free); - int num_addrs = 0; int http_tls_was_enabled = 0; + char *bindaddr = NULL; cfg = ast_config_load2("http.conf", "http", config_flags); if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) { @@ -2169,9 +2458,7 @@ static int __ast_http_load(int reload) v->value, DEFAULT_PORT); } } else if (!strcasecmp(v->name, "bindaddr")) { - if (!(num_addrs = ast_sockaddr_resolve(&addrs, v->value, 0, AST_AF_UNSPEC))) { - ast_log(LOG_WARNING, "Invalid bind address %s\n", v->value); - } + bindaddr = ast_strdupa(v->value); } else if (!strcasecmp(v->name, "prefix")) { if (!ast_strlen_zero(v->value)) { newprefix[0] = '/'; @@ -2213,34 +2500,27 @@ static int __ast_http_load(int reload) ast_copy_string(http_server_name, server_name, sizeof(http_server_name)); - if (num_addrs && enabled) { - int i; - for (i = 0; i < num_addrs; ++i) { - ast_sockaddr_copy(&http_desc.local_address, &addrs[i]); - if (!ast_sockaddr_port(&http_desc.local_address)) { - ast_sockaddr_set_port(&http_desc.local_address, bindport); - } - ast_tcptls_server_start(&http_desc); - if (http_desc.accept_fd == -1) { - ast_log(LOG_WARNING, "Failed to start HTTP server for address %s\n", ast_sockaddr_stringify(&addrs[i])); - ast_sockaddr_setnull(&http_desc.local_address); - } else { - ast_verb(1, "Bound HTTP server to address %s\n", ast_sockaddr_stringify(&addrs[i])); - break; - } - } - /* When no specific TLS bindaddr is specified, we just use - * the non-TLS bindaddress here. + if (enabled) { + http_server_get("http server", bindaddr, bindport, &global_http_server); + } else if (global_http_server) { + http_server_discard(global_http_server); + global_http_server = NULL; + } + + /* When no specific TLS bindaddr is specified, we just use + * the non-TLS bindaddress here. + */ + if (global_http_server && ast_sockaddr_isnull(&https_desc.local_address) && + global_http_server->args.accept_fd != -1) { + + ast_sockaddr_copy(&https_desc.local_address, &global_http_server->args.local_address); + /* Of course, we can't use the same port though. + * Since no bind address was specified, we just use the + * default TLS port */ - if (ast_sockaddr_isnull(&https_desc.local_address) && http_desc.accept_fd != -1) { - ast_sockaddr_copy(&https_desc.local_address, &http_desc.local_address); - /* Of course, we can't use the same port though. - * Since no bind address was specified, we just use the - * default TLS port - */ - ast_sockaddr_set_port(&https_desc.local_address, DEFAULT_TLS_PORT); - } + ast_sockaddr_set_port(&https_desc.local_address, DEFAULT_TLS_PORT); } + if (http_tls_was_enabled && !http_tls_cfg.enabled) { ast_tcptls_server_stop(&https_desc); } else if (http_tls_cfg.enabled && !ast_sockaddr_isnull(&https_desc.local_address)) { @@ -2298,11 +2578,11 @@ static char *handle_show_http(struct ast_cli_entry *e, int cmd, struct ast_cli_a ast_cli(a->fd, "HTTP Server Status:\n"); ast_cli(a->fd, "Prefix: %s\n", prefix); ast_cli(a->fd, "Server: %s\n", http_server_name); - if (ast_sockaddr_isnull(&http_desc.old_address)) { + if (!global_http_server) { ast_cli(a->fd, "Server Disabled\n\n"); } else { ast_cli(a->fd, "Server Enabled and Bound to %s\n\n", - ast_sockaddr_stringify(&http_desc.old_address)); + ast_sockaddr_stringify(&global_http_server->args.old_address)); if (http_tls_cfg.enabled) { ast_cli(a->fd, "HTTPS Server Enabled and Bound to %s\n\n", ast_sockaddr_stringify(&https_desc.old_address)); @@ -2345,7 +2625,9 @@ static int unload_module(void) struct http_uri_redirect *redirect; ast_cli_unregister_multiple(cli_http, ARRAY_LEN(cli_http)); - ast_tcptls_server_stop(&http_desc); + ao2_cleanup(global_http_server); + ao2_cleanup(http_servers); + if (http_tls_cfg.enabled) { ast_tcptls_server_stop(&https_desc); } @@ -2375,7 +2657,19 @@ static int load_module(void) { ast_cli_register_multiple(cli_http, ARRAY_LEN(cli_http)); - return __ast_http_load(0) ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_SUCCESS; + http_servers = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, + HTTP_SERVER_BUCKETS, ast_http_server_hash_fn, NULL, ast_http_server_cmp_fn); + if (!http_servers) { + return AST_MODULE_LOAD_FAILURE; + } + + if (__ast_http_load(0)) { + ao2_cleanup(http_servers); + http_servers = NULL; + return AST_MODULE_LOAD_FAILURE; + } + + return AST_MODULE_LOAD_SUCCESS; } AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Built-in HTTP Server",