
When a request to the REST API is made with invalid JSON, it's hard to see why the request fails due to the fact that no error is sent.
419 lines
11 KiB
C++
419 lines
11 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: 2020-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 <maxscale/atomic.h>
|
|
#include <maxscale/debug.h>
|
|
#include <maxscale/thread.h>
|
|
#include <maxscale/utils.h>
|
|
#include <maxscale/config.h>
|
|
#include <maxscale/hk_heartbeat.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 (url == "/")
|
|
{
|
|
// Respond to pings with 200 OK
|
|
reply = HttpResponse(MHD_HTTP_OK);
|
|
}
|
|
else 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;
|
|
}
|