MaxScale/server/core/admin.cc
Markus Mäkelä 77585bdb8c
MXS-2197: Make config.h and service.h C++ headers
This is the first step into converting the other headers into C++.
2018-11-30 12:15:57 +02:00

433 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(MHD_Connection* connection, string method)
{
return (method == MHD_HTTP_METHOD_POST || method == MHD_HTTP_METHOD_PUT
|| method == MHD_HTTP_METHOD_DELETE || method == MHD_HTTP_METHOD_PATCH)
&& request_data_length(connection);
}
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());
if (request.validate_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 (!admin_user_is_inet_admin(user) && modifies_data(connection, method))
{
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 && modifies_data(connection, method))
{
// 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;
}