MXS-1220: Use libmicrohttpd for the HTTP handling

The HTTP side of the REST API is better handled with an actual
library. The libmicrohttpd library provides a convenient way of handling
the HTTP traffic between the clients and MaxScale.
This commit is contained in:
Markus Mäkelä
2017-04-18 12:19:01 +03:00
committed by Markus Mäkelä
parent eb3ff1cc7b
commit d242203279
14 changed files with 136 additions and 1357 deletions

View File

@ -1,6 +1,5 @@
add_library(maxscale-common SHARED
admin.cc
adminclient.cc
adminusers.cc
alloc.cc
atomic.cc
@ -74,6 +73,7 @@ target_link_libraries(maxscale-common
rt
m
stdc++
microhttpd
)
add_dependencies(maxscale-common pcre2 connector-c)

View File

@ -15,6 +15,7 @@
#include <climits>
#include <new>
#include <microhttpd.h>
#include <maxscale/atomic.h>
#include <maxscale/debug.h>
@ -23,171 +24,66 @@
#include "maxscale/admin.hh"
#include "maxscale/hk_heartbeat.h"
#include "maxscale/resource.hh"
#define DEFAULT_ADMIN_HOST "127.0.0.1"
#define DEFAULT_ADMIN_PORT 8080
#define DEFAULT_ADMIN_AUTH HTTP_AUTH_NONE
static AdminListener* admin = NULL;
static THREAD admin_thread;
static THREAD timeout_thread;
static struct MHD_Daemon* http_daemon = NULL;
// TODO: Read values from the configuration
static AdminConfig config = {DEFAULT_ADMIN_HOST, DEFAULT_ADMIN_PORT, DEFAULT_ADMIN_AUTH};
int handle_client(void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **con_cls)
void admin_main(void* data)
{
AdminListener* admin = reinterpret_cast<AdminListener*>(data);
admin->start();
}
string verb(method);
json_t* json = NULL;
void timeout_main(void *data)
{
AdminListener* admin = reinterpret_cast<AdminListener*>(data);
admin->check_timeouts();
}
if (verb == "POST" || verb == "PUT" || verb == "PATCH")
{
json_error_t err = {};
if ((json = json_loadb(upload_data, *upload_data_size, 0, &err)) == NULL)
{
return MHD_NO;
}
}
AdminConfig& mxs_admin_get_config()
{
return config;
HttpRequest request(connection, url, method, json);
HttpResponse reply = resource_handle_request(request);
string data = reply.get_response();
struct MHD_Response *response = MHD_create_response_from_buffer(data.size(),
(void*)data.c_str(),
MHD_RESPMEM_MUST_COPY);
for (map<string, string>::const_iterator it = reply.get_headers().begin();
it != reply.get_headers().end(); it++)
{
MHD_add_response_header(response, it->first.c_str(), it->second.c_str());
}
MHD_queue_response(connection, reply.get_code(), response);
MHD_destroy_response(response);
return MHD_YES;
}
bool mxs_admin_init()
{
ss_dassert(admin == NULL);
bool rval = false;
struct sockaddr_storage addr = {};
int sock = open_network_socket(MXS_SOCKET_LISTENER, &addr, config.host.c_str(), config.port);
http_daemon = MHD_start_daemon(MHD_USE_EPOLL_INTERNALLY | MHD_USE_DUAL_STACK,
DEFAULT_ADMIN_PORT, NULL, NULL,
handle_client, NULL, MHD_OPTION_END);
return http_daemon != NULL;
if (sock > -1)
{
setblocking(sock);
if (listen(sock, INT_MAX) == 0)
{
admin = new (std::nothrow) AdminListener(sock);
if (admin)
{
if (thread_start(&admin_thread, admin_main, admin) &&
thread_start(&timeout_thread, timeout_main, admin))
{
rval = true;
}
else
{
admin->stop();
delete admin;
admin = NULL;
}
}
else
{
MXS_OOM();
}
}
else
{
MXS_ERROR("Failed to start listening on '[%s]:%u': %d, %s",
config.host.c_str(), config.port, errno, mxs_strerror(errno));
close(sock);
}
}
return rval;
}
void mxs_admin_shutdown()
{
if (admin)
{
admin->stop();
thread_wait(timeout_thread);
thread_wait(admin_thread);
delete admin;
admin = NULL;
}
}
AdminListener::AdminListener(int sock):
m_socket(sock),
m_active(1),
m_timeout(10)
{
}
AdminListener::~AdminListener()
{
close(m_socket);
}
void AdminListener::handle_clients()
{
AdminClient* client;
while ((client = accept_client()))
{
SAdminClient sclient(client);
ClientList::iterator it = m_clients.insert(m_clients.begin(), sclient);
sclient->process();
m_clients.erase(it);
}
}
void AdminListener::start()
{
while (atomic_read(&m_active))
{
MXS_EXCEPTION_GUARD(handle_clients());
}
}
void AdminListener::stop()
{
atomic_write(&m_active, 0);
}
AdminClient* AdminListener::accept_client()
{
AdminClient* rval = NULL;
struct sockaddr_storage addr = {};
socklen_t len = sizeof (addr);
int fd = accept(m_socket, (struct sockaddr*) &addr, &len);
if (fd > -1)
{
rval = new AdminClient(fd, addr, m_timeout);
}
else
{
MXS_ERROR("Failed to accept client: %d, %s\n", errno, mxs_strerror(errno));
}
return rval;
}
void AdminListener::handle_timeouts()
{
int64_t now = hkheartbeat;
for (ClientList::iterator it = m_clients.begin(); it != m_clients.end(); it++)
{
SAdminClient& client = *it;
if (now - client->last_activity() > m_timeout * 10)
{
client->close_connection();
}
}
/** Sleep for roughly one housekeeper heartbeat */
thread_millisleep(100);
}
void AdminListener::check_timeouts()
{
while (atomic_read(&m_active))
{
MXS_EXCEPTION_GUARD(handle_timeouts());
}
MHD_stop_daemon(http_daemon);
}

View File

@ -1,113 +0,0 @@
/*
* 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: 2019-07-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.
*/
#include "maxscale/adminclient.hh"
#include <string>
#include <sstream>
#include <maxscale/atomic.h>
#include <maxscale/hk_heartbeat.h>
#include <maxscale/log_manager.h>
#include <maxscale/thread.h>
#include <maxscale/spinlock.hh>
#include "maxscale/httprequest.hh"
#include "maxscale/httpresponse.hh"
#include "maxscale/resource.hh"
using std::string;
using std::stringstream;
using mxs::SpinLockGuard;
AdminClient::AdminClient(int fd, const struct sockaddr_storage& addr, int timeout):
m_fd(fd),
m_last_activity(atomic_read_int64(&hkheartbeat)),
m_addr(addr)
{
}
void AdminClient::close_connection()
{
SpinLockGuard guard(m_lock);
if (m_fd != -1)
{
close(m_fd);
m_fd = -1;
}
}
AdminClient::~AdminClient()
{
close_connection();
}
static bool read_request(int fd, string& output)
{
while (true)
{
char buf[1024];
int bufsize = sizeof(buf) - 1;
int rc = read(fd, buf, bufsize);
if (rc == -1)
{
return false;
}
buf[rc] = '\0';
output += buf;
if (rc < bufsize)
{
/** Complete request read */
break;
}
}
return true;
}
static bool write_response(int fd, const string& body)
{
return write(fd, body.c_str(), body.length()) != -1;
}
void AdminClient::process()
{
string data;
atomic_write_int64(&m_last_activity, hkheartbeat);
if (read_request(m_fd, data))
{
HttpResponse response(HTTP_400_BAD_REQUEST);
SHttpRequest request(HttpRequest::parse(data));
atomic_write_int64(&m_last_activity, hkheartbeat);
if (request.get())
{
response = resource_handle_request(*request);
}
if (!write_response(m_fd, response.get_response()))
{
MXS_ERROR("Failed to write response to client: %d, %s", errno, mxs_strerror(errno));
}
}
else
{
MXS_ERROR("Failed to read client request: %d, %s", errno, mxs_strerror(errno));
}
}

View File

@ -82,186 +82,13 @@ static void process_uri(string& uri, deque<string>& uri_parts)
}
}
static bool process_options(string& uri, map<string, string>& options)
HttpRequest::HttpRequest(struct MHD_Connection *connection, string url, string method, json_t* data):
m_json(data),
m_resource(url),
m_verb(method),
m_connection(connection)
{
size_t pos = uri.find("?");
if (pos != string::npos)
{
string optionstr = uri.substr(pos + 1);
uri.erase(pos);
char buf[optionstr.size() + 1];
strcpy(buf, optionstr.c_str());
char* saved;
char* tok = strtok_r(buf, ",", &saved);
while (tok && *tok)
{
string opt(tok);
pos = opt.find("=");
if (pos != string::npos)
{
string key = opt.substr(0, pos);
string value = opt.substr(pos + 1);
if (key.length() && value.length())
{
options[key] = value;
}
}
else
{
/** Invalid option */
return false;
}
tok = strtok_r(NULL, ",", &saved);
}
}
return true;
}
static bool process_headers(string& data, map<string, string>& headers)
{
size_t pos;
while ((pos = data.find("\r\n")) != string::npos)
{
string header_line = data.substr(0, pos);
data.erase(0, pos + 2);
if (header_line.length() == 0)
{
/** End of headers */
break;
}
if ((pos = header_line.find(":")) != string::npos)
{
string key = header_line.substr(0, pos);
header_line.erase(0, pos + 1);
headers[key] = mxs::trim(header_line);
}
else
{
/** Invalid header */
return false;
}
}
return true;
}
HttpRequest* HttpRequest::parse(string data)
{
size_t pos = data.find("\r\n");
if (pos == string::npos)
{
return NULL;
}
string request_line = data.substr(0, pos);
data.erase(0, pos + 2);
/** Request method */
if ((pos = request_line.find(" ")) == string::npos)
{
return NULL;
}
string verb = request_line.substr(0, pos);
request_line.erase(0, pos + 1);
/** Get the combined URL/option string */
if ((pos = request_line.find(" ")) == string::npos)
{
return NULL;
}
string uri = request_line.substr(0, pos);
request_line.erase(0, pos + 1);
map<string, string> options;
/** Process request options */
if (!process_options(uri, options))
{
return NULL;
}
/** Split the URI into separate parts */
deque<string> uri_parts;
process_uri(uri, uri_parts);
pos = request_line.find("\r\n");
string http_version = request_line.substr(0, pos);
request_line.erase(0, pos + 2);
map<string, string> headers;
/** Process request headers */
if (!process_headers(data, headers))
{
return NULL;
}
/**
* The headers are now processed and consumed. The message body is
* the only thing left in the request string and it should be a JSON object.
* Attempt to parse it and return an error if it fails.
*/
bool ok = false;
HttpRequest* request = NULL;
enum http_verb verb_value = string_to_http_verb(verb);
json_error_t json_error = {};
json_t* body = NULL;
/** Remove leading and trailing whitespace */
if (data.length())
{
mxs::trim(data);
}
if (http_version == "HTTP/1.1" && verb_value != HTTP_UNKNOWN)
{
if (data.length() && (body = json_loads(data.c_str(), 0, &json_error)) == NULL)
{
MXS_DEBUG("JSON error in input on line %d column %d: %s (%s)",
json_error.line, json_error.column, json_error.text,
data.c_str());
}
else
{
ok = true;
}
}
if (ok)
{
request = new HttpRequest();
request->m_headers = headers;
request->m_options = options;
request->m_json.reset(body);
request->m_json_string = data;
request->m_resource = uri;
request->m_resource_parts = uri_parts;
request->m_verb = verb_value;
}
return request;
}
HttpRequest::HttpRequest():
m_json(NULL),
m_verb(HTTP_UNKNOWN)
{
process_uri(url, m_resource_parts);
}
HttpRequest::~HttpRequest()

View File

@ -23,7 +23,7 @@
using std::string;
using std::stringstream;
HttpResponse::HttpResponse(enum http_code code, string response):
HttpResponse::HttpResponse(int code, string response):
m_body(response),
m_code(code)
{
@ -33,13 +33,6 @@ HttpResponse::HttpResponse(enum http_code code, string response):
m_headers["Last-Modified"] = m_headers["Date"];
// TODO: Add proper ETags
m_headers["ETag"] = "bm90LXlldC1pbXBsZW1lbnRlZAo=";
enum http_auth auth = mxs_admin_get_config().auth;
if (auth != HTTP_AUTH_NONE)
{
m_headers["WWW-Authenticate"] = http_auth_to_string(auth);
}
}
HttpResponse::~HttpResponse()
@ -50,20 +43,17 @@ void HttpResponse::add_header(string name, string value)
{
m_headers[name] = value;
}
const map<string, string>& HttpResponse::get_headers() const
{
return m_headers;
}
string HttpResponse::get_response() const
{
stringstream response;
response << "HTTP/1.1 " << http_code_to_string(m_code) << "\r\n";
for (map<string, string>::const_iterator it = m_headers.begin();
it != m_headers.end(); it++)
{
response << it->first << ": " << it->second << "\r\n";
}
/** End of headers, add the body */
response << "\r\n" << m_body;
return response.str();
return m_body;
}
int HttpResponse::get_code() const
{
return m_code;
}

View File

@ -20,57 +20,6 @@
#include <maxscale/thread.h>
#include "http.hh"
#include "adminclient.hh"
using std::deque;
using std::string;
typedef deque<SAdminClient> ClientList;
/** The admin interface configuration */
struct AdminConfig
{
string host;
uint16_t port;
enum http_auth auth;
};
class AdminListener
{
public:
/**
* @brief Create a new admin interface instance
*
* @param sock Listener socket for the interface
*/
AdminListener(int sock);
~AdminListener();
/**
* Start the admin interface
*/
void start();
/**
* Stop the admin listener
*/
void stop();
/**
* Close timed out connections
*/
void check_timeouts();
private:
void handle_clients();
void handle_timeouts();
AdminClient* accept_client();
int m_socket; /**< The network socket we listen on */
int m_active; /**< Positive value if the admin is active */
int m_timeout; /**< Network timeout in seconds */
ClientList m_clients;
};
/**
* @brief Start the administrative interface
@ -83,10 +32,3 @@ bool mxs_admin_init();
* @brief Shutdown the administrative interface
*/
void mxs_admin_shutdown();
/**
* @brief Get the administative interface configuration
*
* @return A reference to the administrative interface configuration
*/
AdminConfig& mxs_admin_get_config();

View File

@ -1,68 +0,0 @@
#pragma once
/*
* 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: 2019-07-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.
*/
#include <maxscale/cppdefs.hh>
#include <map>
#include <sys/socket.h>
#include <tr1/memory>
#include <maxscale/atomic.h>
#include <maxscale/spinlock.hh>
using mxs::SpinLock;
class AdminClient
{
public:
/**
* @brief Create a new client connection
*
* @param fd Client socket
* @param addr Network address where @c fd is connected to
* @param timeout Network timeout for reads and writes
*/
AdminClient(int fd, const struct sockaddr_storage& addr, int timeout);
~AdminClient();
/**
* @brief Process one request
*/
void process();
/**
* @brief Close the connection
*/
void close_connection();
/**
* @brief Get last activity timestamp
*
* @return The hkheartbeat of the last activity
*/
int64_t last_activity()
{
return atomic_read_int64(&m_last_activity);
}
private:
int m_fd; /**< The client socket */
int64_t m_last_activity;
struct sockaddr_storage m_addr; /**< Network info for the client */
SpinLock m_lock;
};
typedef std::shared_ptr<AdminClient> SAdminClient;

View File

@ -20,245 +20,6 @@
using std::string;
/** Supported HTTP verbs */
enum http_verb
{
HTTP_UNKNOWN,
HTTP_GET,
HTTP_PUT,
HTTP_POST,
HTTP_OPTIONS,
HTTP_PATCH,
HTTP_HEAD
};
/** Possible HTTP return codes */
enum http_code
{
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_202_ACCEPTED,
HTTP_204_NO_CONTENT,
HTTP_301_MOVED_PERMANENTLY,
HTTP_302_FOUND,
HTTP_303_SEE_OTHER,
HTTP_304_NOT_MODIFIED,
HTTP_307_TEMPORARY_REDIRECT,
HTTP_308_PERMANENT_REDIRECT,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_405_METHOD_NOT_ALLOWED,
HTTP_406_NOT_ACCEPTABLE,
HTTP_409_CONFLICT,
HTTP_411_LENGTH_REQUIRED,
HTTP_412_PRECONDITION_FAILED,
HTTP_413_PAYLOAD_TOO_LARGE,
HTTP_414_URI_TOO_LONG,
HTTP_415_UNSUPPORTED_MEDIA_TYPE,
HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_423_LOCKED,
HTTP_428_PRECONDITION_REQUIRED,
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE,
HTTP_500_INTERNAL_SERVER_ERROR,
HTTP_501_NOT_IMPLEMENTED,
HTTP_502_BAD_GATEWAY,
HTTP_503_SERVICE_UNAVAILABLE,
HTTP_504_GATEWAY_TIMEOUT,
HTTP_505_HTTP_VERSION_NOT_SUPPORTED,
HTTP_506_VARIANT_ALSO_NEGOTIATES,
HTTP_507_INSUFFICIENT_STORAGE,
HTTP_508_LOOP_DETECTED,
HTTP_510_NOT_EXTENDED
};
/** Supported authentication types */
enum http_auth
{
HTTP_AUTH_NONE,
HTTP_AUTH_BASIC,
HTTP_AUTH_DIGEST
} ;
/**
* @brief Convert auth type to string
*
* @param auth Authentication value to convert
*
* @return Converted value in string format
*/
static inline const char* http_auth_to_string(enum http_auth auth)
{
switch (auth)
{
case HTTP_AUTH_NONE:
return "None";
case HTTP_AUTH_BASIC:
return "Basic";
case HTTP_AUTH_DIGEST:
return "Digest";
default:
ss_dassert(false);
return "";
}
}
/**
* @brief Convert string to HTTP verb
*
* @param verb String containing HTTP verb
*
* @return Enum value of the verb
*/
static inline enum http_verb string_to_http_verb(string& verb)
{
if (verb == "GET")
{
return HTTP_GET;
}
else if (verb == "POST")
{
return HTTP_POST;
}
else if (verb == "PUT")
{
return HTTP_PUT;
}
else if (verb == "PATCH")
{
return HTTP_PATCH;
}
else if (verb == "OPTIONS")
{
return HTTP_OPTIONS;
}
else if (verb == "HEAD")
{
return HTTP_HEAD;
}
return HTTP_UNKNOWN;
}
/**
* @brief Convert HTTP verb enum to string
*
* @param verb Enum to convert
*
* @return String representation of the enum
*/
static inline const char* http_verb_to_string(enum http_verb verb)
{
switch (verb)
{
case HTTP_GET:
return "GET";
case HTTP_POST:
return "POST";
case HTTP_PUT:
return "PUT";
case HTTP_PATCH:
return "PATCH";
case HTTP_OPTIONS:
return "OPTIONS";
case HTTP_HEAD:
return "HEAD";
default:
return "UNKNOWN";
}
}
/**
* @brief Convert HTTP code to string
*
* @param code The code to convert
*
* @return The HTTP/1.1 string version of the code
*/
static inline const char* http_code_to_string(enum http_code code)
{
switch (code)
{
case HTTP_200_OK:
return "200 OK";
case HTTP_201_CREATED:
return "201 Created";
case HTTP_202_ACCEPTED:
return "202 Accepted";
case HTTP_204_NO_CONTENT:
return "204 No Content";
case HTTP_301_MOVED_PERMANENTLY:
return "301 Moved Permanently";
case HTTP_302_FOUND:
return "302 Found";
case HTTP_303_SEE_OTHER:
return "303 See Other";
case HTTP_304_NOT_MODIFIED:
return "304 Not Modified";
case HTTP_307_TEMPORARY_REDIRECT:
return "307 Temporary Redirect";
case HTTP_308_PERMANENT_REDIRECT:
return "308 Permanent Redirect";
case HTTP_400_BAD_REQUEST:
return "400 Bad Request";
case HTTP_401_UNAUTHORIZED:
return "401 Unauthorized";
case HTTP_403_FORBIDDEN:
return "403 Forbidden";
case HTTP_404_NOT_FOUND:
return "404 Not Found";
case HTTP_405_METHOD_NOT_ALLOWED:
return "405 Method Not Allowed";
case HTTP_406_NOT_ACCEPTABLE:
return "406 Not Acceptable";
case HTTP_409_CONFLICT:
return "409 Conflict";
case HTTP_411_LENGTH_REQUIRED:
return "411 Length Required";
case HTTP_412_PRECONDITION_FAILED:
return "412 Precondition Failed";
case HTTP_413_PAYLOAD_TOO_LARGE:
return "413 Payload Too Large";
case HTTP_414_URI_TOO_LONG:
return "414 URI Too Long";
case HTTP_415_UNSUPPORTED_MEDIA_TYPE:
return "415 Unsupported Media Type";
case HTTP_422_UNPROCESSABLE_ENTITY:
return "422 Unprocessable Entity";
case HTTP_423_LOCKED:
return "423 Locked";
case HTTP_428_PRECONDITION_REQUIRED:
return "428 Precondition Required";
case HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE:
return "431 Request Header Fields Too Large";
case HTTP_500_INTERNAL_SERVER_ERROR:
return "500 Internal Server Error";
case HTTP_501_NOT_IMPLEMENTED:
return "501 Not Implemented";
case HTTP_502_BAD_GATEWAY:
return "502 Bad Gateway";
case HTTP_503_SERVICE_UNAVAILABLE:
return "503 Service Unavailable";
case HTTP_504_GATEWAY_TIMEOUT:
return "504 Gateway Timeout";
case HTTP_505_HTTP_VERSION_NOT_SUPPORTED:
return "505 HTTP Version Not Supported";
case HTTP_506_VARIANT_ALSO_NEGOTIATES:
return "506 Variant Also Negotiates";
case HTTP_507_INSUFFICIENT_STORAGE:
return "507 Insufficient Storage";
case HTTP_508_LOOP_DETECTED:
return "508 Loop Detected";
case HTTP_510_NOT_EXTENDED:
return "510 Not Extended";
default:
ss_dassert(false);
return "500 Internal Server Error";
}
}
/**
* @brief Return the current HTTP-date
*

View File

@ -19,6 +19,7 @@
#include <string>
#include <tr1/memory>
#include <cstdint>
#include <microhttpd.h>
#include <maxscale/jansson.hh>
#include <maxscale/utils.hh>
@ -36,6 +37,22 @@ class HttpRequest;
/** Typedef for managed pointer */
typedef std::shared_ptr<HttpRequest> SHttpRequest;
static int value_iterator(void *cls,
enum MHD_ValueKind kind,
const char *key,
const char *value)
{
std::pair<string, string>* cmp = (std::pair<string, string>*)cls;
if (cmp->first == key)
{
cmp->second = value;
return MHD_NO;
}
return MHD_YES;
}
class HttpRequest
{
public:
@ -46,7 +63,7 @@ public:
*
* @return Parsed statement or NULL if request is not valid
*/
static HttpRequest* parse(string request);
HttpRequest(struct MHD_Connection *connection, string url, string method, json_t* data);
~HttpRequest();
@ -55,23 +72,11 @@ public:
*
* @return One of the HTTP verb values
*/
enum http_verb get_verb() const
const string& get_verb() const
{
return m_verb;
}
/**
* @brief Check if a request contains the specified header
*
* @param header Header to check
*
* @return True if header is in the request
*/
bool have_header(const string& header) const
{
return m_headers.find(header) != m_headers.end();
}
/**
* @brief Get header value
*
@ -81,15 +86,13 @@ public:
*/
string get_header(const string header) const
{
string rval;
map<string, string>::const_iterator it = m_headers.find(header);
std::pair<string, string> p;
p.first = header;
if (it != m_headers.end())
{
rval = it->second;
}
MHD_get_connection_values(m_connection, MHD_HEADER_KIND,
value_iterator, &p);
return rval;
return p.second;
}
/**
@ -101,15 +104,13 @@ public:
*/
string get_option(const string option) const
{
string rval;
map<string, string>::const_iterator it = m_options.find(option);
std::pair<string, string> p;
p.first = option;
if (it != m_options.end())
{
rval = it->second;
}
MHD_get_connection_values(m_connection, MHD_GET_ARGUMENT_KIND,
value_iterator, &p);
return rval;
return p.second;
}
/**
@ -165,15 +166,12 @@ public:
}
private:
HttpRequest();
HttpRequest(const HttpRequest&);
HttpRequest& operator = (const HttpRequest&);
map<string, string> m_headers; /**< Request headers */
map<string, string> m_options; /**< Request options */
Closer<json_t*> m_json; /**< Request body */
string m_json_string; /**< String version of @c m_json */
string m_resource; /**< Requested resource */
deque<string> m_resource_parts; /**< @c m_resource split into parts */
enum http_verb m_verb; /**< Request method */
string m_verb; /**< Request method */
struct MHD_Connection* m_connection;
};

View File

@ -17,6 +17,7 @@
#include <map>
#include <string>
#include <tr1/memory>
#include <microhttpd.h>
#include <maxscale/jansson.hh>
@ -39,7 +40,7 @@ public:
* @param response Response body
* @param code HTTP return code
*/
HttpResponse(enum http_code code = HTTP_200_OK, string response = "");
HttpResponse(int code = MHD_HTTP_OK, string response = "");
~HttpResponse();
@ -51,6 +52,13 @@ public:
*/
void add_header(string name, string value);
/**
* @brief Get headers for this response
*
* @return Map of headers and values
*/
const map<string, string>& get_headers() const;
/**
* @brief Get the response in string format
*
@ -58,8 +66,15 @@ public:
*/
string get_response() const;
/**
* @brief Get the HTTP response code
*
* @return The HTTP response code
*/
int get_code() const;
private:
string m_body; /**< Message body */
map<string, string> m_headers; /**< Message headers */
enum http_code m_code; /**< The HTTP code for the response */
int m_code; /**< The HTTP code for the response */
};

View File

@ -60,7 +60,7 @@ protected:
virtual HttpResponse handle(HttpRequest& request)
{
ss_dassert(false);
return HttpResponse(HTTP_500_INTERNAL_SERVER_ERROR);
return HttpResponse(MHD_HTTP_INTERNAL_SERVER_ERROR);
};
/**

View File

@ -50,7 +50,7 @@ protected:
{
// TODO: Generate this via the inter-thread messaging system
Closer<json_t*> servers(server_list_to_json());
return HttpResponse(HTTP_200_OK, mxs::json_dump(servers, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(servers, flags));
}
else
{
@ -61,11 +61,11 @@ protected:
// TODO: Generate this via the inter-thread messaging system
Closer<json_t*> server_js(server_to_json(server));
// Show one server
return HttpResponse(HTTP_200_OK, mxs::json_dump(server_js, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(server_js, flags));
}
else
{
return HttpResponse(HTTP_404_NOT_FOUND);
return HttpResponse(MHD_HTTP_NOT_FOUND);
}
}
}
@ -80,10 +80,10 @@ protected:
if (request.uri_part_count() == 1)
{
// TODO: Generate this via the inter-thread messaging system
// TODO: Generate this via the inter-thread messaging system
Closer<json_t*> all_services(service_list_to_json());
// Show all services
return HttpResponse(HTTP_200_OK, mxs::json_dump(all_services, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(all_services, flags));
}
else
{
@ -93,11 +93,11 @@ protected:
{
Closer<json_t*> service_js(service_to_json(service));
// Show one service
return HttpResponse(HTTP_200_OK, mxs::json_dump(service_js, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(service_js, flags));
}
else
{
return HttpResponse(HTTP_404_NOT_FOUND);
return HttpResponse(MHD_HTTP_NOT_FOUND);
}
}
}
@ -114,7 +114,7 @@ protected:
{
Closer<json_t*> filters(filter_list_to_json());
// Show all filters
return HttpResponse(HTTP_200_OK, mxs::json_dump(filters, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(filters, flags));
}
else
{
@ -124,11 +124,11 @@ protected:
{
Closer<json_t*> filter_js(filter_to_json(filter));
// Show one filter
return HttpResponse(HTTP_200_OK, mxs::json_dump(filter_js, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(filter_js, flags));
}
else
{
return HttpResponse(HTTP_404_NOT_FOUND);
return HttpResponse(MHD_HTTP_NOT_FOUND);
}
}
}
@ -145,7 +145,7 @@ protected:
{
Closer<json_t*> monitors(monitor_list_to_json());
// Show all monitors
return HttpResponse(HTTP_200_OK, mxs::json_dump(monitors, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(monitors, flags));
}
else
{
@ -155,11 +155,11 @@ protected:
{
Closer<json_t*> monitor_js(monitor_to_json(monitor));
// Show one monitor
return HttpResponse(HTTP_200_OK, mxs::json_dump(monitor_js, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(monitor_js, flags));
}
else
{
return HttpResponse(HTTP_404_NOT_FOUND);
return HttpResponse(MHD_HTTP_NOT_FOUND);
}
}
}
@ -173,7 +173,7 @@ protected:
if (request.uri_part_count() == 1)
{
// Show all sessions
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
else
{
@ -187,11 +187,11 @@ protected:
Closer<json_t*> ses_json(session_to_json(session));
session_put_ref(session);
// Show session statistics
return HttpResponse(HTTP_200_OK, mxs::json_dump(ses_json, flags));
return HttpResponse(MHD_HTTP_OK, mxs::json_dump(ses_json, flags));
}
else
{
return HttpResponse(HTTP_404_NOT_FOUND);
return HttpResponse(MHD_HTTP_NOT_FOUND);
}
}
}
@ -202,7 +202,7 @@ class UsersResource: public Resource
protected:
HttpResponse handle(HttpRequest& request)
{
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};
@ -216,17 +216,17 @@ protected:
// Flush logs
if (mxs_log_rotate() == 0)
{
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
else
{
return HttpResponse(HTTP_500_INTERNAL_SERVER_ERROR);
return HttpResponse(MHD_HTTP_INTERNAL_SERVER_ERROR);
}
}
else
{
// Show log status
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
}
};
@ -237,7 +237,7 @@ protected:
HttpResponse handle(HttpRequest& request)
{
// Show thread status
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};
@ -247,7 +247,7 @@ protected:
HttpResponse handle(HttpRequest& request)
{
// Show housekeeper tasks
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};
@ -257,7 +257,7 @@ protected:
HttpResponse handle(HttpRequest& request)
{
// Show modules
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};
@ -275,7 +275,7 @@ public:
protected:
HttpResponse handle(HttpRequest& request)
{
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};
@ -296,7 +296,7 @@ public:
protected:
HttpResponse handle(HttpRequest& request)
{
return HttpResponse(HTTP_200_OK);
return HttpResponse(MHD_HTTP_OK);
}
};

View File

@ -23,7 +23,6 @@ add_executable(testmaxscalepcre2 testmaxscalepcre2.cc)
add_executable(testmodulecmd testmodulecmd.cc)
add_executable(testconfig testconfig.cc)
add_executable(trxboundaryparser_profile trxboundaryparser_profile.cc)
add_executable(testhttp testhttp.cc)
target_link_libraries(test_atomic maxscale-common)
target_link_libraries(test_adminusers maxscale-common)
target_link_libraries(test_buffer maxscale-common)
@ -49,7 +48,6 @@ target_link_libraries(testmaxscalepcre2 maxscale-common)
target_link_libraries(testmodulecmd maxscale-common)
target_link_libraries(testconfig maxscale-common)
target_link_libraries(trxboundaryparser_profile maxscale-common)
target_link_libraries(testhttp maxscale-common)
add_test(TestAtomic test_atomic)
add_test(TestAdminUsers test_adminusers)
add_test(TestBuffer test_buffer)
@ -81,7 +79,6 @@ add_test(TestTrxCompare_Select test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../..
add_test(TestTrxCompare_Set test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/set.test)
add_test(TestTrxCompare_Update test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/update.test)
add_test(TestTrxCompare_MaxScale test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/maxscale.test)
add_test(TestHttp testhttp)
# This test requires external dependencies and thus cannot be run
# as a part of the core test set

View File

@ -1,466 +0,0 @@
/*
* 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: 2019-07-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.
*/
#include "../maxscale/httprequest.hh"
#include "../maxscale/httpresponse.hh"
#include <sstream>
using std::stringstream;
using std::string;
#define TEST(a, b, ...) do{if (!(a)){printf(b "\n", ##__VA_ARGS__);return 1;}}while(false)
const char* verbs_pass[] =
{
"GET",
"PUT",
"POST",
"OPTIONS",
"PATCH",
"HEAD",
NULL
};
const char* verbs_fail[] =
{
"LOAD",
"STORE",
"PUBLISH",
"Something that's not a verb",
"",
NULL
};
static struct
{
const char* input;
const char* output;
} paths_pass[] =
{
{ "/", "/" },
{ "*", "*" },
{ "/test/", "/test" },
{ "/test", "/test" },
{ "/servers/list", "/servers/list" },
{ "/servers/list/", "/servers/list" },
{ "/?test=true", "/" },
{ "/test/?test=y", "/test" },
{ "/?", "/" },
{}
};
const char* paths_fail[] =
{
"-strikethrough-",
"_underline_",
"*bold*",
"?",
NULL
};
const char* proto_pass[] =
{
"HTTP/1.1",
NULL
};
const char* proto_fail[] =
{
"HTTP/2.0",
"SMTP/0.0",
"CDC/1.0",
NULL
};
int test_basic()
{
/** Test parts that should pass */
for (int i = 0; verbs_pass[i]; i++)
{
for (int j = 0; paths_pass[j].input; j++)
{
for (int k = 0; proto_pass[k]; k++)
{
stringstream ss;
ss << verbs_pass[i] << " " << paths_pass[j].input << " " << proto_pass[k] << "\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() != NULL, "Valid HTTP request should be parsed: %s", ss.str().c_str());
TEST(parser->get_uri() == string(paths_pass[j].output),
"The request path '%s' should be correct: %s",
paths_pass[j].output, parser->get_uri().c_str());
}
}
}
/** Test parts that should fail */
for (int i = 0; verbs_fail[i]; i++)
{
for (int j = 0; paths_fail[j]; j++)
{
for (int k = 0; proto_fail[k]; k++)
{
stringstream ss;
ss << verbs_fail[i] << " " << paths_fail[j] << " " << proto_fail[k] << "\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() == NULL, "Invalid HTTP request should not be parsed: %s", ss.str().c_str());
}
}
}
return 0;
}
static struct
{
const char* key;
const char* value;
} headers_pass[] =
{
{"Accept", "*/*"},
{"User-Agent", "curl/7.51.0"},
{"Authorization", "bWF4dXNlcjptYXhwd2QK"},
{"Content-Type", "application/json"},
{"Date", "1.1.2017 10:10:10"},
{"Host", "127.0.0.1:8080"},
{"If-Match", "bWF4dXNlcjptYXhwd2QK"},
{"If-Modified-Since", "Mon, 18 Nov 2013 08:14:29 -0600"},
{"If-None-Match", "bWF4dXNlcjptYXhwd2QK"},
{"If-Unmodified-Since", "Mon, 18 Nov 2013 08:14:29 -0600"},
{"X-HTTP-Method-Override", "PATCH"},
{"Allow", "GET, PATCH, PUT"},
{"Accept-Patch", "application/json-patch"},
{"Date", "Mon, 18 Nov 2013 08:14:29 -0600"},
{"ETag", "bWF4dXNlcjptYXhwd2QK"},
{"Last-Modified", "Mon, 18 Nov 2013 08:14:29 -0600"},
{"Location", "/servers/server1"},
{"WWW-Authenticate", "Basic"},
{0}
};
int test_headers()
{
for (int i = 0; headers_pass[i].key; i++)
{
stringstream ss;
ss << "GET / HTTP/1.1\r\n" << headers_pass[i].key << ": "
<< headers_pass[i].value << "\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() != NULL, "Valid HTTP request should be parsed: %s", ss.str().c_str());
TEST(parser->get_header(headers_pass[i].key).length() > 0, "Header should be found");
TEST(parser->get_header(headers_pass[i].key) == string(headers_pass[i].value),
"Header value should be correct: %s", parser->get_header(headers_pass[i].key).c_str());
}
return 0;
}
/**
The following JSON tests are imported from the Jansson test suite
*/
const char* body_pass[] =
{
"{\"i\": [1]}",
"{\"i\": [1.8011670033376514e-308]}",
"{\"i\": [123.456e78]}",
"{\"i\": [-1]}",
"{\"i\": [-123]}",
"{\"i\": [\"\u0821 three-byte UTF-8\"]}",
"{\"i\": [123]}",
"{\"i\": [1E+2]}",
"{\"i\": [123e45]}",
"{\"i\": [false]}",
"{\"i\": [\"\u002c one-byte UTF-8\"]}",
"{\"i\": {\"a\":[]}}",
"{\"i\": [\"abcdefghijklmnopqrstuvwxyz1234567890 \"]}",
"{\"i\": [-0]}",
"{\"i\": [\"\"]}",
"{\"i\": [1,2,3,4]}",
"{\"i\": [\"a\", \"b\", \"c\"]}",
"{\"foo\": \"bar\", \"core\": \"dump\"}",
"{\"i\": [true, false, true, true, null, false]}",
"{\"b\": [\"a\"]}",
"{\"i\": [true]}",
"{\"i\": {}}",
"{\"i\": [{}]}",
"{\"i\": [0]}",
"{\"i\": [123.456789]}",
"{\"i\": [1e+2]}",
"{\"i\": [\"\u0123 two-byte UTF-8\"]}",
"{\"i\": [123e-10000000]}",
"{\"i\": [null]}",
"{\"i\": [\"€þıœəßð some utf-8 ĸʒ×ŋµåäö𝄞\"]}",
"{\"i\": [1e-2]}",
"{\"i\": [1E22]}",
"{\"i\": [1E-2]}",
"{\"i\": []}",
/** Additional tests */
"{\"this is\": \"a JSON value\"}",
NULL
};
const char* body_fail[] =
{
"{{}",
"{[-123foo]}",
"{[1,}",
"{[troo}",
"{{\"a\"}",
"{[-123123123123123123123123123123]}",
"{{[}",
"{[1.]}",
"{[1ea]}",
"{['}",
"{[-012]}",
"{[012]}",
"{{\"a}",
"{[{}",
"{[123123123123123123123123123123]}",
"{[1,2,3]}",
"{foo}",
"{[\"\a <-- invalid escape\"]}",
"{[{}}",
"{[\" <-- tab character\"]}",
"{[\"a\"}",
"{{'a'}",
"{[,}",
"{{\"a\":}",
"{{\"a\":\"a}",
"{[-123123e100000]}",
"{[\"null escape \u0000 not allowed\"]}",
"{[1,}",
"{2,}",
"{3,}",
"{4,}",
"{5,}",
"{]}",
"{null}",
"{[-123.123foo]}",
"{[}",
"{aå}",
"{{\"foo\u0000bar\": 42}{\"a\":\"a\" 123}}",
"{[\"a}",
"{[123123e100000]}",
"{[1e]}",
"{[1,]}",
"{{,}",
"{[-foo]}",
"{å}",
"{{\"}",
"{[\"null byte not allowed\"]}",
"{[}",
"{[1,2,3]foo}",
/** Additional tests */
"Hello World!",
"<p>I am a paragraph</p>",
"",
NULL
};
const char* body_verbs_pass[] =
{
"PUT",
"POST",
"PATCH",
NULL
};
int test_message_body()
{
for (int i = 0; body_pass[i]; i++)
{
for (int j = 0; body_verbs_pass[j]; j++)
{
/** Only PUT/POST/PATCH methods should have request bodies */
stringstream ss;
ss << body_verbs_pass[j] << " / HTTP/1.1\r\n\r\n" << body_pass[i];
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() != NULL, "Valid request body should be parsed: %s",
ss.str().c_str());
TEST(parser->get_json(), "Body should be found");
TEST(parser->get_json_str() == body_pass[i], "Body value should be correct: %s",
parser->get_json_str().c_str());
}
}
for (int i = 0; body_pass[i]; i++)
{
for (int j = 0; verbs_pass[j]; j++)
{
stringstream ss;
ss << verbs_pass[j] << " / HTTP/1.1\r\n\r\n" << body_fail[i];
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() == NULL, "Invalid request body should not be parsed: %s",
ss.str().c_str());
}
}
return 0;
}
static struct
{
const char* input;
const char* key;
const char* value;
} options_pass[] =
{
{ "/", "", "" },
{ "*", "", "" },
{ "/?a=b", "a", "b" },
{ "/?a=b,c=d", "a", "b" },
{ "/?a=b,c=d", "c", "d" },
{ "/test?q=w", "q", "w" },
{ "/servers/list?all=false", "all", "false" },
{ "/servers/list/?pretty=true", "pretty", "true"},
{ "/?test=true", "test", "true" },
{ "/test/?test=y", "test", "y" },
{ "/?", "", "" },
{}
};
const char* options_fail[] =
{
"/?,",
"/??",
"/test?/",
"/test/?a,b",
"/test?a,",
NULL
};
int test_options()
{
for (int i = 0; options_pass[i].input; i++)
{
stringstream ss;
ss << "GET " << options_pass[i].input << " HTTP/1.1\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() != NULL, "Valid option should be parsed: %s", ss.str().c_str());
TEST(parser->get_option(options_pass[i].key) == options_pass[i].value,
"The option value for '%s' should be '%s': %s",
options_pass[i].key, options_pass[i].value,
parser->get_option(options_pass[i].key).c_str());
}
for (int i = 0; options_fail[i]; i++)
{
stringstream ss;
ss << "GET " << options_fail[i] << " HTTP/1.1\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() == NULL, "Invalid option should not be parsed: %s", ss.str().c_str());
}
return 0;
}
int test_response()
{
TEST(HttpResponse().get_response().find("200 OK") != string::npos,
"Default constructor should return a 200 OK with no body");
TEST(HttpResponse(HTTP_200_OK, "Test").get_response().find("\r\n\r\nTest") != string::npos,
"Custom payload should be found in the response");
TEST(HttpResponse(HTTP_204_NO_CONTENT).get_response().find("204 No Content") != string::npos,
"Using custom header should generate correct response");
HttpResponse response(HTTP_502_BAD_GATEWAY, "A Bad gateway");
TEST(response.get_response().find("\r\n\r\nA Bad gateway") != string::npos &&
response.get_response().find("502 Bad Gateway") != string::npos,
"Both custom response body and return code should be found");
return 0;
}
struct
{
const char* input;
const char* value;
int offset;
size_t size;
} resource_parts[] =
{
{"/", "", 0, 1},
{"/?a=b", "", 0, 1},
{"/servers/", "servers", 0, 1},
{"/servers", "servers", 0, 1},
{"servers", "servers", 0, 1},
{"/servers/my-server", "servers", 0, 2},
{"/servers/my-server/", "servers", 0, 2},
{"servers/my-server", "servers", 0, 2},
{"/servers/my-server", "my-server", 1, 2},
{"/servers/my-server/", "my-server", 1, 2},
{"servers/my-server", "my-server", 1, 2},
{"/servers/my-server/user", "servers", 0, 3},
{"/servers/my-server/user", "servers", 0, 3},
{"servers/my-server/user", "servers", 0, 3},
{"/servers/my-server/user", "my-server", 1, 3},
{"/servers/my-server/user/", "my-server", 1, 3},
{"servers/my-server/user", "my-server", 1, 3},
{"/servers/my-server/user", "user", 2, 3},
{"/servers/my-server/user/", "user", 2, 3},
{"servers/my-server/user", "user", 2, 3},
{"/servers?a=b", "servers", 0, 1},
{"/servers/?a=b", "servers", 0, 1},
{"servers/?a=b", "servers", 0, 1},
{"/servers/my-server?a=b", "my-server", 1, 2},
{"/servers/my-server/?a=b", "my-server", 1, 2},
{"servers/my-server/?a=b", "my-server", 1, 2},
{"/servers/my-server/user?a=b", "user", 2, 3},
{"/servers/my-server/user/?a=b", "user", 2, 3},
{"servers/my-server/user?a=b", "user", 2, 3},
{}
};
int test_resource_parts()
{
for (int i = 0; resource_parts[i].input; i++)
{
stringstream ss;
ss << "GET " << resource_parts[i].input << " HTTP/1.1\r\n\r\n";
SHttpRequest request(HttpRequest::parse(ss.str()));
TEST(request.get(), "Request should be OK: %s", ss.str().c_str());
TEST(request->uri_part_count() == resource_parts[i].size,
"Request should have %lu parts: %lu", resource_parts[i].size,
request->uri_part_count());
string value = request->uri_part(resource_parts[i].offset);
TEST(value == resource_parts[i].value,
"Request part at %d should be '%s': %s", resource_parts[i].offset,
resource_parts[i].value, value.c_str());
}
return 0;
}
int main(int argc, char** argv)
{
int rc = 0;
rc += test_basic();
rc += test_headers();
rc += test_message_body();
rc += test_response();
rc += test_resource_parts();
return rc;
}