/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2012 - 2013, Digium, Inc. * * David M. Lee, II * * 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 HTTP binding for the Stasis API * \author David M. Lee, II * * The API itself is documented using Swagger, a lightweight * mechanism for documenting RESTful API's using JSON. This allows us to use swagger-ui to provide * executable documentation for the API, generate client bindings in different * languages, * and generate a lot of the boilerplate code for implementing the RESTful * bindings. The API docs live in the \c rest-api/ directory. * * The RESTful bindings are generated from the Swagger API docs using a set of * Mustache templates. * The code generator is written in Python, and uses the Python implementation * pystache. Pystache has no * dependencies, and be installed easily using \c pip. Code generation code * lives in \c rest-api-templates/. * * The generated code reduces a lot of boilerplate when it comes to handling * HTTP requests. It also helps us have greater consistency in the REST API. * * The structure of the generated code is: * * - res/ari/resource_{resource}.h * - For each operation in the resource, a generated argument structure * (holding the parsed arguments from the request) and function * declarations (to implement in res/ari/resource_{resource}.c) * - res_ari_{resource}.c * - A set of \ref stasis_rest_callback functions, which glue the two * together. They parse out path variables and request parameters to * populate a specific \c *_args which is passed to the specific request * handler (in res/ari/resource_{resource}.c) * - A tree of \ref stasis_rest_handlers for routing requests to its * \ref stasis_rest_callback * * The basic flow of an HTTP request is: * * - ast_ari_callback() * 1. Initial request validation * 2. Routes as either a doc request (ast_ari_get_docs) or API * request (ast_ari_invoke) * - ast_ari_invoke() * 1. Further request validation * 2. Routes the request through the tree of generated * \ref stasis_rest_handlers. * 3. Dispatch to the generated callback * - \c ast_ari_*_cb * 1. Populate \c *_args struct with path and get params * 2. Invoke the request handler * 3. Validates and sends response */ /*** MODULEINFO res_http_websocket res_stasis res_websocket_client core ***/ #include "asterisk.h" #include "ari/internal.h" #include "ari/ari_websockets.h" #include "asterisk/ari.h" #include "asterisk/astobj2.h" #include "asterisk/module.h" #include "asterisk/paths.h" #include "asterisk/stasis_app.h" #include #include #include /*! \brief Helper function to check if module is enabled. */ static int is_enabled(void) { RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); return general && general->enabled; } /*! Lock for \ref root_handler */ static ast_mutex_t root_handler_lock; /*! Handler for root RESTful resource. */ static struct stasis_rest_handlers *root_handler; /*! Pre-defined message for allocation failures. */ static struct ast_json *oom_json; /*! \brief Callback for the root URI. */ static int ast_ari_callback(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_params, struct ast_variable *headers); static struct ast_http_uri http_uri = { .callback = ast_ari_callback, .description = "Asterisk RESTful API", .uri = "ari", .has_subtree = 1, .data = NULL, .key = __FILE__, .no_decode_uri = 1, }; struct ast_json *ast_ari_oom_json(void) { return oom_json; } int ast_ari_add_handler(struct stasis_rest_handlers *handler) { RAII_VAR(struct stasis_rest_handlers *, new_handler, NULL, ao2_cleanup); size_t old_size, new_size; SCOPED_MUTEX(lock, &root_handler_lock); old_size = sizeof(*new_handler) + root_handler->num_children * sizeof(handler); new_size = old_size + sizeof(handler); new_handler = ao2_alloc(new_size, NULL); if (!new_handler) { return -1; } memcpy(new_handler, root_handler, old_size); new_handler->children[new_handler->num_children++] = handler; ao2_cleanup(root_handler); ao2_ref(new_handler, +1); root_handler = new_handler; return 0; } int ast_ari_remove_handler(struct stasis_rest_handlers *handler) { struct stasis_rest_handlers *new_handler; size_t size; size_t i; size_t j; ast_assert(root_handler != NULL); ast_mutex_lock(&root_handler_lock); size = sizeof(*new_handler) + root_handler->num_children * sizeof(handler); new_handler = ao2_alloc(size, NULL); if (!new_handler) { ast_mutex_unlock(&root_handler_lock); return -1; } /* Create replacement root_handler less the handler to remove. */ memcpy(new_handler, root_handler, sizeof(*new_handler)); for (i = 0, j = 0; i < root_handler->num_children; ++i) { if (root_handler->children[i] == handler) { continue; } new_handler->children[j++] = root_handler->children[i]; } new_handler->num_children = j; /* Replace the old root_handler with the new. */ ao2_cleanup(root_handler); root_handler = new_handler; ast_mutex_unlock(&root_handler_lock); return 0; } static struct stasis_rest_handlers *get_root_handler(void) { SCOPED_MUTEX(lock, &root_handler_lock); ao2_ref(root_handler, +1); return root_handler; } static struct stasis_rest_handlers *root_handler_create(void) { RAII_VAR(struct stasis_rest_handlers *, handler, NULL, ao2_cleanup); handler = ao2_alloc(sizeof(*handler), NULL); if (!handler) { return NULL; } handler->path_segment = "ari"; ao2_ref(handler, +1); return handler; } void ast_ari_response_error(struct ast_ari_response *response, int response_code, const char *response_text, const char *message_fmt, ...) { RAII_VAR(struct ast_json *, message, NULL, ast_json_unref); va_list ap; va_start(ap, message_fmt); message = ast_json_vstringf(message_fmt, ap); va_end(ap); response->message = ast_json_pack("{s: o}", "message", ast_json_ref(message)); response->response_code = response_code; response->response_text = response_text; } void ast_ari_response_ok(struct ast_ari_response *response, struct ast_json *message) { response->message = message; response->response_code = 200; response->response_text = "OK"; } void ast_ari_response_no_content(struct ast_ari_response *response) { response->message = ast_json_null(); response->response_code = 204; response->response_text = "No Content"; } void ast_ari_response_accepted(struct ast_ari_response *response) { response->message = ast_json_null(); response->response_code = 202; response->response_text = "Accepted"; } void ast_ari_response_alloc_failed(struct ast_ari_response *response) { response->message = ast_json_ref(oom_json); response->response_code = 500; response->response_text = "Internal Server Error"; } void ast_ari_response_created(struct ast_ari_response *response, const char *url, struct ast_json *message) { RAII_VAR(struct stasis_rest_handlers *, root, get_root_handler(), ao2_cleanup); response->message = message; response->response_code = 201; response->response_text = "Created"; ast_str_append(&response->headers, 0, "Location: /%s%s\r\n", root->path_segment, url); } static void add_allow_header(struct stasis_rest_handlers *handler, struct ast_ari_response *response) { enum ast_http_method m; ast_str_append(&response->headers, 0, "Allow: OPTIONS"); for (m = 0; m < AST_HTTP_MAX_METHOD; ++m) { if (handler->callbacks[m] != NULL) { ast_str_append(&response->headers, 0, ",%s", ast_get_http_method(m)); } } ast_str_append(&response->headers, 0, "\r\n"); } static int origin_allowed(const char *origin) { RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); char *allowed = ast_strdupa(general ? general->allowed_origins : ""); char *current; while ((current = strsep(&allowed, ","))) { if (!strcmp(current, "*")) { return 1; } if (!strcmp(current, origin)) { return 1; } } return 0; } #define ACR_METHOD "Access-Control-Request-Method" #define ACR_HEADERS "Access-Control-Request-Headers" #define ACA_METHODS "Access-Control-Allow-Methods" #define ACA_HEADERS "Access-Control-Allow-Headers" /*! * \brief Handle OPTIONS request, mainly for CORS preflight requests. * * Some browsers will send this prior to non-simple methods (i.e. DELETE). * See http://www.w3.org/TR/cors/ for the spec. Especially section 6.2. */ static void handle_options(struct stasis_rest_handlers *handler, struct ast_variable *headers, struct ast_ari_response *response) { struct ast_variable *header; char const *acr_method = NULL; char const *acr_headers = NULL; char const *origin = NULL; RAII_VAR(struct ast_str *, allow, NULL, ast_free); enum ast_http_method m; int allowed = 0; /* Regular OPTIONS response */ add_allow_header(handler, response); ast_ari_response_no_content(response); /* Parse CORS headers */ for (header = headers; header != NULL; header = header->next) { if (strcmp(ACR_METHOD, header->name) == 0) { acr_method = header->value; } else if (strcmp(ACR_HEADERS, header->name) == 0) { acr_headers = header->value; } else if (strcmp("Origin", header->name) == 0) { origin = header->value; } } /* CORS 6.2, #1 - "If the Origin header is not present terminate this * set of steps." */ if (origin == NULL) { return; } /* CORS 6.2, #2 - "If the value of the Origin header is not a * case-sensitive match for any of the values in list of origins do not * set any additional headers and terminate this set of steps. * * Always matching is acceptable since the list of origins can be * unbounded. * * The Origin header can only contain a single origin as the user agent * will not follow redirects." */ if (!origin_allowed(origin)) { ast_log(LOG_NOTICE, "Origin header '%s' does not match an allowed origin.\n", origin); return; } /* CORS 6.2, #3 - "If there is no Access-Control-Request-Method header * or if parsing failed, do not set any additional headers and terminate * this set of steps." */ if (acr_method == NULL) { return; } /* CORS 6.2, #4 - "If there are no Access-Control-Request-Headers * headers let header field-names be the empty list." */ if (acr_headers == NULL) { acr_headers = ""; } /* CORS 6.2, #5 - "If method is not a case-sensitive match for any of * the values in list of methods do not set any additional headers and * terminate this set of steps." */ allow = ast_str_create(20); if (!allow) { ast_ari_response_alloc_failed(response); return; } /* Go ahead and build the ACA_METHODS header at the same time */ for (m = 0; m < AST_HTTP_MAX_METHOD; ++m) { if (handler->callbacks[m] != NULL) { char const *m_str = ast_get_http_method(m); if (strcmp(m_str, acr_method) == 0) { allowed = 1; } ast_str_append(&allow, 0, ",%s", m_str); } } if (!allowed) { return; } /* CORS 6.2 #6 - "If any of the header field-names is not a ASCII * case-insensitive match for any of the values in list of headers do * not set any additional headers and terminate this set of steps. * * Note: Always matching is acceptable since the list of headers can be * unbounded." */ /* CORS 6.2 #7 - "If the resource supports credentials add a single * Access-Control-Allow-Origin header, with the value of the Origin * header as value, and add a single Access-Control-Allow-Credentials * header with the case-sensitive string "true" as value." * * Added by process_cors_request() earlier in the request. */ /* CORS 6.2 #8 - "Optionally add a single Access-Control-Max-Age * header..." */ /* CORS 6.2 #9 - "Add one or more Access-Control-Allow-Methods headers * consisting of (a subset of) the list of methods." */ ast_str_append(&response->headers, 0, "%s: OPTIONS%s\r\n", ACA_METHODS, ast_str_buffer(allow)); /* CORS 6.2, #10 - "Add one or more Access-Control-Allow-Headers headers * consisting of (a subset of) the list of headers. * * Since the list of headers can be unbounded simply returning headers * can be enough." */ if (!ast_strlen_zero(acr_headers)) { ast_str_append(&response->headers, 0, "%s: %s\r\n", ACA_HEADERS, acr_headers); } } /*! * \brief Authenticate a ?api_key=userid:password * * \param api_key API key query parameter * \return User object for the authenticated user. * \retval NULL if authentication failed. */ static struct ari_conf_user *authenticate_api_key(const char *api_key) { RAII_VAR(char *, copy, NULL, ast_free); char *username; char *password; password = copy = ast_strdup(api_key); if (!copy) { return NULL; } username = strsep(&password, ":"); if (!password) { ast_log(LOG_WARNING, "Invalid api_key\n"); return NULL; } return ari_conf_validate_user(username, password); } /*! * \brief Authenticate an HTTP request. * * \param get_params GET parameters of the request. * \param headers HTTP headers. * \return User object for the authenticated user. * \retval NULL if authentication failed. */ static struct ari_conf_user *authenticate_user(struct ast_variable *get_params, struct ast_variable *headers) { RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup); struct ast_variable *v; /* HTTP Basic authentication */ http_auth = ast_http_get_auth(headers); if (http_auth) { return ari_conf_validate_user(http_auth->userid, http_auth->password); } /* ?api_key authentication */ for (v = get_params; v; v = v->next) { if (strcasecmp("api_key", v->name) == 0) { return authenticate_api_key(v->value); } } return NULL; } static void remove_trailing_slash(const char *uri, struct ast_ari_response *response) { char *slashless = ast_strdupa(uri); slashless[strlen(slashless) - 1] = '\0'; /* While it's tempting to redirect the client to the slashless URL, * that is problematic. A 302 Found is the most appropriate response, * but most clients issue a GET on the location you give them, * regardless of the method of the original request. * * While there are some ways around this, it gets into a lot of client * specific behavior and corner cases in the HTTP standard. There's also * very little practical benefit of redirecting; only GET and HEAD can * be redirected automagically; all other requests "MUST NOT * automatically redirect the request unless it can be confirmed by the * user, since this might change the conditions under which the request * was issued." * * Given all of that, a 404 with a nice message telling them what to do * is probably our best bet. */ ast_ari_response_error(response, 404, "Not Found", "ARI URLs do not end with a slash. Try /ari/%s", slashless); } enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *ser, enum ast_ari_invoke_source source, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_params, struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response) { RAII_VAR(struct stasis_rest_handlers *, root, NULL, ao2_cleanup); struct stasis_rest_handlers *handler = NULL; struct stasis_rest_handlers *wildcard_handler = NULL; RAII_VAR(struct ast_variable *, path_vars, NULL, ast_variables_destroy); RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup); RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); char *path = ast_strdupa(uri); char *path_segment = NULL; stasis_rest_callback callback; SCOPE_ENTER(3, "Request: %s %s, path:%s\n", ast_get_http_method(method), uri, path); if (!general) { if (ser && source == ARI_INVOKE_SOURCE_REST) { ast_http_request_close_on_completion(ser); } ast_ari_response_error(response, 500, "Server Error", "URI handler config missing"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CLOSE, "Response: %d : %s\n", response->response_code, response->response_text); } user = authenticate_user(get_params, headers); if (!user && source == ARI_INVOKE_SOURCE_REST) { /* Per RFC 2617, section 1.2: The 401 (Unauthorized) response * message is used by an origin server to challenge the * authorization of a user agent. This response MUST include a * WWW-Authenticate header field containing at least one * challenge applicable to the requested resource. */ ast_ari_response_error(response, 401, "Unauthorized", "Authentication required"); /* Section 1.2: * realm = "realm" "=" realm-value * realm-value = quoted-string * Section 2: * challenge = "Basic" realm */ ast_str_append(&response->headers, 0, "WWW-Authenticate: Basic realm=\"%s\"\r\n", general->auth_realm); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } else if (!ast_fully_booted) { ast_ari_response_error(response, 503, "Service Unavailable", "Asterisk not booted"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CLOSE, "Response: %d : %s\n", response->response_code, response->response_text); } else if (user && user->read_only && method != AST_HTTP_GET && method != AST_HTTP_OPTIONS) { ast_ari_response_error(response, 403, "Forbidden", "Write access denied"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } else if (ast_ends_with(uri, "/")) { remove_trailing_slash(uri, response); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } else if (ast_begins_with(uri, "api-docs/")) { /* Serving up API docs */ if (method != AST_HTTP_GET) { ast_ari_response_error(response, 405, "Method Not Allowed", "Unsupported method"); } else { if (urih) { /* Skip the api-docs prefix */ ast_ari_get_docs(strchr(uri, '/') + 1, urih->prefix, headers, response); } else { /* * If we were invoked without a urih, we're probably * being called from the websocket so just use the * default prefix. It's filled in by ast_http_uri_link(). */ ast_ari_get_docs(strchr(uri, '/') + 1, http_uri.prefix, headers, response); } } SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } root = handler = get_root_handler(); ast_assert(root != NULL); while ((path_segment = strsep(&path, "/")) && (strlen(path_segment) > 0)) { struct stasis_rest_handlers *found_handler = NULL; int i; SCOPE_ENTER(4, "Finding handler for path segment %s\n", path_segment); ast_uri_decode(path_segment, ast_uri_http_legacy); for (i = 0; found_handler == NULL && i < handler->num_children; ++i) { struct stasis_rest_handlers *child = handler->children[i]; SCOPE_ENTER(5, "Checking handler path segment %s\n", child->path_segment); if (child->is_wildcard) { /* Record the path variable */ struct ast_variable *path_var = ast_variable_new(child->path_segment, path_segment, __FILE__); path_var->next = path_vars; path_vars = path_var; wildcard_handler = child; ast_trace(-1, " Checking %s %s: Matched wildcard.\n", handler->path_segment, child->path_segment); } else if (strcmp(child->path_segment, path_segment) == 0) { found_handler = child; ast_trace(-1, " Checking %s %s: Explicit match with %s\n", handler->path_segment, child->path_segment, path_segment); } else { ast_trace(-1, " Checking %s %s: Didn't match %s\n", handler->path_segment, child->path_segment, path_segment); } SCOPE_EXIT("Done checking %s\n", child->path_segment); } if (!found_handler && wildcard_handler) { ast_trace(-1, " No explicit handler found for %s. Using wildcard %s.\n", path_segment, wildcard_handler->path_segment); found_handler = wildcard_handler; wildcard_handler = NULL; } if (found_handler == NULL) { /* resource not found */ ast_ari_response_error( response, 404, "Not Found", "Resource not found"); SCOPE_EXIT_EXPR(break, "Handler not found for %s\n", path_segment); } else { handler = found_handler; } SCOPE_EXIT("Done checking %s\n", path_segment); } if (handler == NULL || response->response_code == 404) { /* resource not found */ SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s %s\n", response->response_code, response->response_text, uri); } ast_assert(handler != NULL); if (method == AST_HTTP_OPTIONS) { handle_options(handler, headers, response); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Was options\n"); } if (method < 0 || method >= AST_HTTP_MAX_METHOD) { add_allow_header(handler, response); ast_ari_response_error( response, 405, "Method Not Allowed", "Invalid method"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } if (handler->is_websocket && method == AST_HTTP_GET) { if (source == ARI_INVOKE_SOURCE_WEBSOCKET) { ast_ari_response_error( response, 400, "Bad request", "Can't upgrade to websocket from a websocket"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } /* WebSocket! */ ast_trace(-1, "Handling websocket %s\n", uri); ari_handle_websocket(ser, uri, method, get_params, headers); /* Since the WebSocket code handles the connection, we shouldn't * do anything else; setting no_response */ response->no_response = 1; SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Upgrade to websocket\n"); } callback = handler->callbacks[method]; if (callback == NULL) { add_allow_header(handler, response); ast_ari_response_error( response, 405, "Method Not Allowed", "Invalid method"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } ast_trace(-1, "Running callback: %s\n", uri); callback(ser, get_params, path_vars, headers, body, response); if (response->message == NULL && response->response_code == 0) { /* Really should not happen */ ast_log(LOG_ERROR, "ARI %s %s not implemented\n", ast_get_http_method(method), uri); ast_ari_response_error( response, 501, "Not Implemented", "Method not implemented"); SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n", response->response_code, response->response_text); } SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_SUCCESS, "Response: %d : %s\n", response->response_code, response->response_text); } void ast_ari_get_docs(const char *uri, const char *prefix, struct ast_variable *headers, struct ast_ari_response *response) { RAII_VAR(struct ast_str *, absolute_path_builder, NULL, ast_free); RAII_VAR(char *, absolute_api_dirname, NULL, ast_std_free); RAII_VAR(char *, absolute_filename, NULL, ast_std_free); struct ast_json *obj = NULL; struct ast_variable *host = NULL; struct ast_json_error error = {}; struct stat file_stat; ast_debug(3, "%s(%s)\n", __func__, uri); absolute_path_builder = ast_str_create(80); if (absolute_path_builder == NULL) { ast_ari_response_alloc_failed(response); return; } /* absolute path to the rest-api directory */ ast_str_append(&absolute_path_builder, 0, "%s", ast_config_AST_DATA_DIR); ast_str_append(&absolute_path_builder, 0, "/rest-api/"); absolute_api_dirname = realpath(ast_str_buffer(absolute_path_builder), NULL); if (absolute_api_dirname == NULL) { ast_log(LOG_ERROR, "Error determining real directory for rest-api\n"); ast_ari_response_error( response, 500, "Internal Server Error", "Cannot find rest-api directory"); return; } /* absolute path to the requested file */ ast_str_append(&absolute_path_builder, 0, "%s", uri); absolute_filename = realpath(ast_str_buffer(absolute_path_builder), NULL); if (absolute_filename == NULL) { switch (errno) { case ENAMETOOLONG: case ENOENT: case ENOTDIR: ast_ari_response_error( response, 404, "Not Found", "Resource not found"); break; case EACCES: ast_ari_response_error( response, 403, "Forbidden", "Permission denied"); break; default: ast_log(LOG_ERROR, "Error determining real path for uri '%s': %s\n", uri, strerror(errno)); ast_ari_response_error( response, 500, "Internal Server Error", "Cannot find file"); break; } return; } if (!ast_begins_with(absolute_filename, absolute_api_dirname)) { /* HACKERZ! */ ast_log(LOG_ERROR, "Invalid attempt to access '%s' (not in %s)\n", absolute_filename, absolute_api_dirname); ast_ari_response_error( response, 404, "Not Found", "Resource not found"); return; } if (stat(absolute_filename, &file_stat) == 0) { if (!(file_stat.st_mode & S_IFREG)) { /* Not a file */ ast_ari_response_error( response, 403, "Forbidden", "Invalid access"); return; } } else { /* Does not exist */ ast_ari_response_error( response, 404, "Not Found", "Resource not found"); return; } /* Load resource object from file */ obj = ast_json_load_new_file(absolute_filename, &error); if (obj == NULL) { ast_log(LOG_ERROR, "Error parsing resource file: %s:%d(%d) %s\n", error.source, error.line, error.column, error.text); ast_ari_response_error( response, 500, "Internal Server Error", "Yikes! Cannot parse resource"); return; } /* Update the basePath properly */ if (ast_json_object_get(obj, "basePath") != NULL) { for (host = headers; host; host = host->next) { if (strcasecmp(host->name, "Host") == 0) { break; } } if (host != NULL) { if (prefix != NULL && strlen(prefix) > 0) { ast_json_object_set( obj, "basePath", ast_json_stringf("http://%s%s/ari", host->value,prefix)); } else { ast_json_object_set( obj, "basePath", ast_json_stringf("http://%s/ari", host->value)); } } else { /* Without the host, we don't have the basePath */ ast_json_object_del(obj, "basePath"); } } ast_ari_response_ok(response, obj); } /*! * \brief Handle CORS headers for simple requests. * * See http://www.w3.org/TR/cors/ for the spec. Especially section 6.1. */ static void process_cors_request(struct ast_variable *headers, struct ast_ari_response *response) { char const *origin = NULL; struct ast_variable *header; /* Parse CORS headers */ for (header = headers; header != NULL; header = header->next) { if (strcmp("Origin", header->name) == 0) { origin = header->value; } } /* CORS 6.1, #1 - "If the Origin header is not present terminate this * set of steps." */ if (origin == NULL) { return; } /* CORS 6.1, #2 - "If the value of the Origin header is not a * case-sensitive match for any of the values in list of origins, do not * set any additional headers and terminate this set of steps. * * Note: Always matching is acceptable since the list of origins can be * unbounded." */ if (!origin_allowed(origin)) { ast_log(LOG_NOTICE, "Origin header '%s' does not match an allowed origin.\n", origin); return; } /* CORS 6.1, #3 - "If the resource supports credentials add a single * Access-Control-Allow-Origin header, with the value of the Origin * header as value, and add a single Access-Control-Allow-Credentials * header with the case-sensitive string "true" as value. * * Otherwise, add a single Access-Control-Allow-Origin header, with * either the value of the Origin header or the string "*" as value." */ ast_str_append(&response->headers, 0, "Access-Control-Allow-Origin: %s\r\n", origin); ast_str_append(&response->headers, 0, "Access-Control-Allow-Credentials: true\r\n"); /* CORS 6.1, #4 - "If the list of exposed headers is not empty add one * or more Access-Control-Expose-Headers headers, with as values the * header field names given in the list of exposed headers." * * No exposed headers; skipping */ } enum ast_json_encoding_format ast_ari_json_format(void) { RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup); return general ? general->format : AST_JSON_COMPACT; } /*! * \internal * \brief ARI HTTP handler. * * This handler takes the HTTP request and turns it into the appropriate * RESTful request (conversion to JSON, routing, etc.) * * \param ser TCP session. * \param urih URI handler. * \param uri URI requested. * \param method HTTP method. * \param get_params HTTP \c GET params. * \param headers HTTP headers. */ static int ast_ari_callback(struct ast_tcptls_session_instance *ser, const struct ast_http_uri *urih, const char *uri, enum ast_http_method method, struct ast_variable *get_params, struct ast_variable *headers) { RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free); struct ast_ari_response response = { .fd = -1, 0 }; RAII_VAR(struct ast_variable *, post_vars, NULL, ast_variables_destroy); struct ast_variable *var; const char *app_name = NULL; RAII_VAR(struct ast_json *, body, ast_json_null(), ast_json_unref); int debug_app = 0; enum ast_ari_invoke_result result; SCOPE_ENTER(2, "%s: Request: %s %s\n", ast_sockaddr_stringify(&ser->remote_address), ast_get_http_method(method), uri); if (!response_body) { ast_http_request_close_on_completion(ser); ast_http_error(ser, 500, "Server Error", "Out of memory"); SCOPE_EXIT_RTN_VALUE(0, "Out of memory\n"); } response.headers = ast_str_create(40); if (!response.headers) { ast_http_request_close_on_completion(ser); ast_http_error(ser, 500, "Server Error", "Out of memory"); SCOPE_EXIT_RTN_VALUE(0, "Out of memory\n"); } process_cors_request(headers, &response); /* Process form data from a POST. It could be mixed with query * parameters, which seems a bit odd. But it's allowed, so that's okay * with us. */ post_vars = ast_http_get_post_vars(ser, headers); if (!post_vars) { ast_trace(-1, "No post_vars\n"); switch (errno) { case EFBIG: ast_ari_response_error(&response, 413, "Request Entity Too Large", "Request body too large"); goto request_failed; case ENOMEM: ast_http_request_close_on_completion(ser); ast_ari_response_error(&response, 500, "Internal Server Error", "Out of memory"); goto request_failed; case EIO: ast_ari_response_error(&response, 400, "Bad Request", "Error parsing request body"); goto request_failed; } /* Look for a JSON request entity only if there were no post_vars. * If there were post_vars, then the request body would already have * been consumed and can not be read again. */ ast_trace(-1, "Checking body for vars\n"); body = ast_http_get_json(ser, headers); if (!body) { switch (errno) { case EFBIG: ast_ari_response_error(&response, 413, "Request Entity Too Large", "Request body too large"); goto request_failed; case ENOMEM: ast_ari_response_error(&response, 500, "Internal Server Error", "Error processing request"); goto request_failed; case EIO: ast_ari_response_error(&response, 400, "Bad Request", "Error parsing request body"); goto request_failed; } } } if (get_params == NULL) { ast_trace(-1, "No get_params, using post_vars if any\n"); get_params = post_vars; } else if (get_params && post_vars) { /* Has both post_vars and get_params */ struct ast_variable *last_var = post_vars; ast_trace(-1, "Has get_params and post_vars. Merging\n"); while (last_var->next) { last_var = last_var->next; } /* The duped get_params will get freed when post_vars gets * ast_variables_destroyed. */ last_var->next = ast_variables_dup(get_params); get_params = post_vars; } /* At this point, get_params will contain post_vars (if any) */ app_name = ast_variable_find_in_list(get_params, "app"); if (!app_name) { struct ast_json *app = ast_json_object_get(body, "app"); app_name = (app ? ast_json_string_get(app) : NULL); } ast_trace(-1, "app_name: %s\n", app_name); /* stasis_app_get_debug_by_name returns an "||" of the app's debug flag * and the global debug flag. */ debug_app = stasis_app_get_debug_by_name(app_name); if (debug_app) { struct ast_str *buf = ast_str_create(512); char *str = ast_json_dump_string_format(body, ast_ari_json_format()); if (!buf || (body && !str)) { ast_http_request_close_on_completion(ser); ast_ari_response_error(&response, 500, "Server Error", "Out of memory"); ast_json_free(str); ast_free(buf); goto request_failed; } ast_str_append(&buf, 0, "<--- ARI request received from: %s --->\n", ast_sockaddr_stringify(&ser->remote_address)); ast_str_append(&buf, 0, "%s %s\n", ast_get_http_method(method), uri); for (var = headers; var; var = var->next) { ast_str_append(&buf, 0, "%s: %s\n", var->name, var->value); } for (var = get_params; var; var = var->next) { ast_str_append(&buf, 0, "%s: %s\n", var->name, var->value); } ast_verbose("%sbody:\n%s\n\n", ast_str_buffer(buf), S_OR(str, "")); ast_json_free(str); ast_free(buf); } result = SCOPE_CALL_WITH_RESULT(-1, enum ast_ari_invoke_result, ast_ari_invoke, ser, ARI_INVOKE_SOURCE_REST, urih, uri, method, get_params, headers, body, &response); if (result == ARI_INVOKE_RESULT_ERROR_CLOSE) { ast_http_request_close_on_completion(ser); } if (response.no_response) { /* The handler indicates no further response is necessary. * Probably because it already handled it */ ast_free(response.headers); SCOPE_EXIT_RTN_VALUE(0, "No response needed\n"); } request_failed: /* If you explicitly want to have no content, set message to * ast_json_null(). */ ast_assert(response.message != NULL); ast_assert(response.response_code > 0); /* response.message could be NULL, in which case the empty response_body * is correct */ if (response.message && !ast_json_is_null(response.message)) { ast_str_append(&response.headers, 0, "Content-type: application/json\r\n"); if (ast_json_dump_str_format(response.message, &response_body, ast_ari_json_format()) != 0) { /* Error encoding response */ response.response_code = 500; response.response_text = "Internal Server Error"; ast_str_set(&response_body, 0, "%s", ""); ast_str_set(&response.headers, 0, "%s", ""); } } if (debug_app) { ast_verbose("<--- Sending ARI response to %s --->\n%d %s\n%s%s\n\n", ast_sockaddr_stringify(&ser->remote_address), response.response_code, response.response_text, ast_str_buffer(response.headers), ast_str_buffer(response_body)); } ast_http_send(ser, method, response.response_code, response.response_text, response.headers, response_body, response.fd != -1 ? response.fd : 0, 0); /* ast_http_send takes ownership, so we don't have to free them */ response_body = NULL; ast_json_unref(response.message); if (response.fd >= 0) { close(response.fd); } SCOPE_EXIT_RTN_VALUE(0, "Done. response: %d : %s\n", response.response_code, response.response_text); } static int unload_module(void) { ari_websocket_unload_module(); ari_cli_unregister(); if (is_enabled()) { ast_debug(3, "Disabling ARI\n"); ast_http_uri_unlink(&http_uri); } ari_conf_destroy(); ao2_cleanup(root_handler); root_handler = NULL; ast_mutex_destroy(&root_handler_lock); ast_json_unref(oom_json); oom_json = NULL; return 0; } static int load_module(void) { ast_mutex_init(&root_handler_lock); /* root_handler may have been built during a declined load */ if (!root_handler) { root_handler = root_handler_create(); } if (!root_handler) { return AST_MODULE_LOAD_DECLINE; } /* oom_json may have been built during a declined load */ if (!oom_json) { oom_json = ast_json_pack( "{s: s}", "error", "Allocation failed"); } if (!oom_json) { /* Ironic */ unload_module(); return AST_MODULE_LOAD_DECLINE; } /* * ari_websocket_load_module() needs to know if ARI is enabled * globally so it needs the "general" config to be loaded but it * also needs to register a sorcery object observer for * "outbound_websocket" BEFORE the outbound_websocket configs are loaded. * outbound_websocket in turn needs the users to be loaded so we'll * initialize sorcery and load "general" and "user" configs first, then * load the websocket module, then load the "outbound_websocket" configs * which will fire the observers. */ if (ari_conf_load(ARI_CONF_INIT | ARI_CONF_LOAD_GENERAL | ARI_CONF_LOAD_USER) != 0) { unload_module(); return AST_MODULE_LOAD_DECLINE; } if (ari_websocket_load_module(is_enabled()) != AST_MODULE_LOAD_SUCCESS) { unload_module(); return AST_MODULE_LOAD_DECLINE; } /* * Now we can load the outbound_websocket configs which will * fire the observers. */ ari_conf_load(ARI_CONF_LOAD_OWC); if (ari_cli_register() != 0) { unload_module(); return AST_MODULE_LOAD_DECLINE; } if (is_enabled()) { ast_debug(3, "ARI enabled\n"); ast_http_uri_link(&http_uri); } else { ast_debug(3, "ARI disabled\n"); } return AST_MODULE_LOAD_SUCCESS; } static int reload_module(void) { char was_enabled = is_enabled(); int is_now_enabled = 0; ari_conf_load(ARI_CONF_RELOAD | ARI_CONF_LOAD_ALL); is_now_enabled = is_enabled(); if (was_enabled && !is_now_enabled) { ast_debug(3, "Disabling ARI\n"); ast_http_uri_unlink(&http_uri); } else if (!was_enabled && is_now_enabled) { ast_debug(3, "Enabling ARI\n"); ast_http_uri_link(&http_uri); } return AST_MODULE_LOAD_SUCCESS; } AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Asterisk RESTful Interface", .support_level = AST_MODULE_SUPPORT_CORE, .load = load_module, .unload = unload_module, .reload = reload_module, .requires = "http,res_stasis,res_http_websocket,res_websocket_client", .load_pri = AST_MODPRI_APP_DEPEND, );