 3e41a601f8
			
		
	
	3e41a601f8
	
	
	
		
			
			The request API prefix is now simply ignored as it has no meaning as long as there is a single version of the API.
		
			
				
	
	
		
			429 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			429 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2016 MariaDB Corporation Ab
 | |
|  *
 | |
|  * Use of this software is governed by the Business Source License included
 | |
|  * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 | |
|  *
 | |
|  * Change Date: 2022-01-01
 | |
|  *
 | |
|  * On the date above, in accordance with the Business Source License, use
 | |
|  * of this software will be governed by version 2 or later of the General
 | |
|  * Public License.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @file The embedded HTTP protocol administrative interface
 | |
|  */
 | |
| #include "internal/admin.hh"
 | |
| 
 | |
| #include <climits>
 | |
| #include <new>
 | |
| #include <fstream>
 | |
| #include <microhttpd.h>
 | |
| #include <sys/stat.h>
 | |
| #include <sys/types.h>
 | |
| #include <sys/socket.h>
 | |
| #include <netdb.h>
 | |
| #include <sys/stat.h>
 | |
| 
 | |
| #include <maxbase/atomic.h>
 | |
| #include <maxbase/assert.h>
 | |
| #include <maxscale/utils.h>
 | |
| #include <maxscale/config.hh>
 | |
| #include <maxscale/clock.h>
 | |
| #include <maxscale/http.hh>
 | |
| #include <maxscale/adminusers.h>
 | |
| 
 | |
| #include "internal/resource.hh"
 | |
| 
 | |
| using std::string;
 | |
| using std::ifstream;
 | |
| 
 | |
| static struct MHD_Daemon* http_daemon = NULL;
 | |
| 
 | |
| /** In-memory certificates in PEM format */
 | |
| static char* admin_ssl_key = NULL;
 | |
| static char* admin_ssl_cert = NULL;
 | |
| static char* admin_ssl_ca_cert = NULL;
 | |
| 
 | |
| static bool using_ssl = false;
 | |
| 
 | |
| int kv_iter(void* cls,
 | |
|             enum MHD_ValueKind kind,
 | |
|             const char* key,
 | |
|             const char* value)
 | |
| {
 | |
|     size_t* rval = (size_t*)cls;
 | |
| 
 | |
|     if (strcasecmp(key, "Content-Length") == 0)
 | |
|     {
 | |
|         *rval = atoi(value);
 | |
|         return MHD_NO;
 | |
|     }
 | |
| 
 | |
|     return MHD_YES;
 | |
| }
 | |
| 
 | |
| static inline size_t request_data_length(MHD_Connection* connection)
 | |
| {
 | |
|     size_t rval = 0;
 | |
|     MHD_get_connection_values(connection, MHD_HEADER_KIND, kv_iter, &rval);
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| static bool modifies_data(const string& method)
 | |
| {
 | |
|     return (method == MHD_HTTP_METHOD_POST || method == MHD_HTTP_METHOD_PUT
 | |
|             || method == MHD_HTTP_METHOD_DELETE || method == MHD_HTTP_METHOD_PATCH);
 | |
| }
 | |
| 
 | |
| static void send_auth_error(MHD_Connection* connection)
 | |
| {
 | |
|     static char error_resp[] = "{\"errors\": [ { \"detail\": \"Access denied\" } ] }";
 | |
|     MHD_Response* resp =
 | |
|         MHD_create_response_from_buffer(sizeof(error_resp) - 1,
 | |
|                                         error_resp,
 | |
|                                         MHD_RESPMEM_PERSISTENT);
 | |
| 
 | |
|     MHD_queue_basic_auth_fail_response(connection, "maxscale", resp);
 | |
|     MHD_destroy_response(resp);
 | |
| }
 | |
| 
 | |
| int Client::process(string url, string method, const char* upload_data, size_t* upload_size)
 | |
| {
 | |
|     json_t* json = NULL;
 | |
| 
 | |
|     if (*upload_size)
 | |
|     {
 | |
|         m_data.append(upload_data, *upload_size);
 | |
|         *upload_size = 0;
 | |
|         return MHD_YES;
 | |
|     }
 | |
| 
 | |
|     json_error_t err = {};
 | |
| 
 | |
|     if (m_data.length()
 | |
|         && (json = json_loadb(m_data.c_str(), m_data.size(), 0, &err)) == NULL)
 | |
|     {
 | |
|         string msg = string("{\"errors\": [ { \"detail\": \"Invalid JSON in request: ")
 | |
|             + err.text + "\" } ] }";
 | |
|         MHD_Response* response = MHD_create_response_from_buffer(msg.size(),
 | |
|                                                                  &msg[0],
 | |
|                                                                  MHD_RESPMEM_MUST_COPY);
 | |
|         MHD_queue_response(m_connection, MHD_HTTP_BAD_REQUEST, response);
 | |
|         MHD_destroy_response(response);
 | |
|         return MHD_YES;
 | |
|     }
 | |
| 
 | |
|     HttpRequest request(m_connection, url, method, json);
 | |
|     HttpResponse reply(MHD_HTTP_NOT_FOUND);
 | |
| 
 | |
|     MXS_DEBUG("Request:\n%s", request.to_string().c_str());
 | |
|     request.fix_api_version();
 | |
|     reply = resource_handle_request(request);
 | |
| 
 | |
|     string data;
 | |
| 
 | |
|     json_t* js = reply.get_response();
 | |
| 
 | |
|     if (js)
 | |
|     {
 | |
|         int flags = 0;
 | |
|         string pretty = request.get_option("pretty");
 | |
| 
 | |
|         if (pretty == "true" || pretty.length() == 0)
 | |
|         {
 | |
|             flags |= JSON_INDENT(4);
 | |
|         }
 | |
| 
 | |
|         data = mxs::json_dump(js, flags);
 | |
|     }
 | |
| 
 | |
|     MHD_Response* response =
 | |
|         MHD_create_response_from_buffer(data.size(),
 | |
|                                         (void*)data.c_str(),
 | |
|                                         MHD_RESPMEM_MUST_COPY);
 | |
| 
 | |
|     const Headers& headers = reply.get_headers();
 | |
| 
 | |
|     for (Headers::const_iterator it = headers.begin(); it != headers.end(); it++)
 | |
|     {
 | |
|         MHD_add_response_header(response, it->first.c_str(), it->second.c_str());
 | |
|     }
 | |
| 
 | |
|     int rval = MHD_queue_response(m_connection, reply.get_code(), response);
 | |
|     MHD_destroy_response(response);
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| void close_client(void* cls,
 | |
|                   MHD_Connection* connection,
 | |
|                   void** con_cls,
 | |
|                   enum MHD_RequestTerminationCode toe)
 | |
| {
 | |
|     Client* client = static_cast<Client*>(*con_cls);
 | |
|     delete client;
 | |
| }
 | |
| 
 | |
| bool Client::auth(MHD_Connection* connection, const char* url, const char* method)
 | |
| {
 | |
|     bool rval = true;
 | |
| 
 | |
|     if (config_get_global_options()->admin_auth)
 | |
|     {
 | |
|         char* pw = NULL;
 | |
|         char* user = MHD_basic_auth_get_username_password(connection, &pw);
 | |
| 
 | |
|         if (!user || !pw || !admin_verify_inet_user(user, pw))
 | |
|         {
 | |
|             if (config_get_global_options()->admin_log_auth_failures)
 | |
|             {
 | |
|                 MXS_WARNING("Authentication failed for '%s', %s. Request: %s %s",
 | |
|                             user ? user : "",
 | |
|                             pw ? "using password" : "no password",
 | |
|                             method,
 | |
|                             url);
 | |
|             }
 | |
|             send_auth_error(connection);
 | |
|             rval = false;
 | |
|         }
 | |
|         else if (modifies_data(method) && !admin_user_is_inet_admin(user, pw))
 | |
|         {
 | |
|             if (config_get_global_options()->admin_log_auth_failures)
 | |
|             {
 | |
|                 MXS_WARNING("Authorization failed for '%s', request requires "
 | |
|                             "administrative privileges. Request: %s %s",
 | |
|                             user,
 | |
|                             method,
 | |
|                             url);
 | |
|             }
 | |
|             rval = false;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_INFO("Accept authentication from '%s', %s. Request: %s",
 | |
|                      user ? user : "",
 | |
|                      pw ? "using password" : "no password",
 | |
|                      url);
 | |
|         }
 | |
|         MXS_FREE(user);
 | |
|         MXS_FREE(pw);
 | |
|     }
 | |
| 
 | |
|     m_state = rval ? Client::OK : Client::FAILED;
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| int handle_client(void* cls,
 | |
|                   MHD_Connection* connection,
 | |
|                   const char* url,
 | |
|                   const char* method,
 | |
|                   const char* version,
 | |
|                   const char* upload_data,
 | |
|                   size_t* upload_data_size,
 | |
|                   void**  con_cls)
 | |
| 
 | |
| {
 | |
|     if (*con_cls == NULL)
 | |
|     {
 | |
|         if ((*con_cls = new(std::nothrow) Client(connection)) == NULL)
 | |
|         {
 | |
|             return MHD_NO;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     Client* client = static_cast<Client*>(*con_cls);
 | |
|     Client::state state = client->get_state();
 | |
|     int rval = MHD_NO;
 | |
| 
 | |
|     if (state != Client::CLOSED)
 | |
|     {
 | |
|         if (state == Client::INIT)
 | |
|         {
 | |
|             // First request, do authentication
 | |
|             if (!client->auth(connection, url, method))
 | |
|             {
 | |
|                 rval = MHD_YES;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (client->get_state() == Client::OK)
 | |
|         {
 | |
|             // Authentication was successful, start processing the request
 | |
|             if (state == Client::INIT && request_data_length(connection))
 | |
|             {
 | |
|                 // The first call doesn't have any data
 | |
|                 rval = MHD_YES;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 rval = client->process(url, method, upload_data, upload_data_size);
 | |
|             }
 | |
|         }
 | |
|         else if (client->get_state() == Client::FAILED)
 | |
|         {
 | |
|             // Authentication has failed, an error will be sent to the client
 | |
|             rval = MHD_YES;
 | |
| 
 | |
|             if (*upload_data_size)
 | |
|             {
 | |
|                 // The client is uploading data, discard it so we can send the error
 | |
|                 *upload_data_size = 0;
 | |
|             }
 | |
|             else if (state != Client::INIT)
 | |
|             {
 | |
|                 // The client has finished uploading data, send an error and close the connection
 | |
|                 send_auth_error(connection);
 | |
|                 client->close();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| static bool host_to_sockaddr(const char* host, uint16_t port, struct sockaddr_storage* addr)
 | |
| {
 | |
|     struct addrinfo* ai = NULL, hint = {};
 | |
|     int rc;
 | |
|     hint.ai_socktype = SOCK_STREAM;
 | |
|     hint.ai_family = AF_UNSPEC;
 | |
|     hint.ai_flags = AI_ALL;
 | |
| 
 | |
|     if ((rc = getaddrinfo(host, NULL, &hint, &ai)) != 0)
 | |
|     {
 | |
|         MXS_ERROR("Failed to obtain address for host %s: %s", host, gai_strerror(rc));
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /* Take the first one */
 | |
|     if (ai)
 | |
|     {
 | |
|         memcpy(addr, ai->ai_addr, ai->ai_addrlen);
 | |
| 
 | |
|         if (addr->ss_family == AF_INET)
 | |
|         {
 | |
|             struct sockaddr_in* ip = (struct sockaddr_in*)addr;
 | |
|             (*ip).sin_port = htons(port);
 | |
|         }
 | |
|         else if (addr->ss_family == AF_INET6)
 | |
|         {
 | |
|             struct sockaddr_in6* ip = (struct sockaddr_in6*)addr;
 | |
|             (*ip).sin6_port = htons(port);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     freeaddrinfo(ai);
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| static char* load_cert(const char* file)
 | |
| {
 | |
|     char* rval = NULL;
 | |
|     ifstream infile(file);
 | |
|     struct stat st;
 | |
| 
 | |
|     if (stat(file, &st) == 0
 | |
|         && (rval = new(std::nothrow) char[st.st_size + 1]))
 | |
|     {
 | |
|         infile.read(rval, st.st_size);
 | |
|         rval[st.st_size] = '\0';
 | |
| 
 | |
|         if (!infile.good())
 | |
|         {
 | |
|             MXS_ERROR("Failed to load certificate file: %s", file);
 | |
|             delete rval;
 | |
|             rval = NULL;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| static bool load_ssl_certificates()
 | |
| {
 | |
|     bool rval = false;
 | |
|     const char* key = config_get_global_options()->admin_ssl_key;
 | |
|     const char* cert = config_get_global_options()->admin_ssl_cert;
 | |
|     const char* ca = config_get_global_options()->admin_ssl_ca_cert;
 | |
| 
 | |
|     if (*key && *cert && *ca)
 | |
|     {
 | |
|         if ((admin_ssl_key = load_cert(key))
 | |
|             && (admin_ssl_cert = load_cert(cert))
 | |
|             && (admin_ssl_ca_cert = load_cert(ca)))
 | |
|         {
 | |
|             rval = true;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             delete admin_ssl_key;
 | |
|             delete admin_ssl_cert;
 | |
|             delete admin_ssl_ca_cert;
 | |
|             admin_ssl_key = NULL;
 | |
|             admin_ssl_cert = NULL;
 | |
|             admin_ssl_ca_cert = NULL;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| bool mxs_admin_init()
 | |
| {
 | |
|     struct sockaddr_storage addr;
 | |
| 
 | |
|     if (host_to_sockaddr(config_get_global_options()->admin_host,
 | |
|                          config_get_global_options()->admin_port,
 | |
|                          &addr))
 | |
|     {
 | |
|         int options = MHD_USE_EPOLL_INTERNALLY_LINUX_ONLY;
 | |
| 
 | |
|         if (addr.ss_family == AF_INET6)
 | |
|         {
 | |
|             options |= MHD_USE_DUAL_STACK;
 | |
|         }
 | |
| 
 | |
|         if (load_ssl_certificates())
 | |
|         {
 | |
|             using_ssl = true;
 | |
|             options |= MHD_USE_SSL;
 | |
|         }
 | |
| 
 | |
|         // The port argument is ignored and the port in the struct sockaddr is used instead
 | |
|         http_daemon = MHD_start_daemon(options,
 | |
|                                        0,
 | |
|                                        NULL,
 | |
|                                        NULL,
 | |
|                                        handle_client,
 | |
|                                        NULL,
 | |
|                                        MHD_OPTION_NOTIFY_COMPLETED,
 | |
|                                        close_client,
 | |
|                                        NULL,
 | |
|                                        MHD_OPTION_SOCK_ADDR,
 | |
|                                        &addr,
 | |
|                                        !using_ssl ? MHD_OPTION_END :
 | |
|                                        MHD_OPTION_HTTPS_MEM_KEY,
 | |
|                                        admin_ssl_key,
 | |
|                                        MHD_OPTION_HTTPS_MEM_CERT,
 | |
|                                        admin_ssl_cert,
 | |
|                                        MHD_OPTION_HTTPS_MEM_TRUST,
 | |
|                                        admin_ssl_cert,
 | |
|                                        MHD_OPTION_END);
 | |
|     }
 | |
| 
 | |
|     return http_daemon != NULL;
 | |
| }
 | |
| 
 | |
| void mxs_admin_shutdown()
 | |
| {
 | |
|     MHD_stop_daemon(http_daemon);
 | |
| }
 | |
| 
 | |
| bool mxs_admin_https_enabled()
 | |
| {
 | |
|     return using_ssl;
 | |
| }
 |