diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt index fa0d3a4d6..736d67e71 100644 --- a/server/core/CMakeLists.txt +++ b/server/core/CMakeLists.txt @@ -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) diff --git a/server/core/admin.cc b/server/core/admin.cc index b7f87b422..3647afd34 100644 --- a/server/core/admin.cc +++ b/server/core/admin.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -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(data); - admin->start(); -} + string verb(method); + json_t* json = NULL; -void timeout_main(void *data) -{ - AdminListener* admin = reinterpret_cast(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::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); } diff --git a/server/core/adminclient.cc b/server/core/adminclient.cc deleted file mode 100644 index d15aa9fe0..000000000 --- a/server/core/adminclient.cc +++ /dev/null @@ -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 -#include - -#include -#include -#include -#include -#include - -#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)); - } -} diff --git a/server/core/httprequest.cc b/server/core/httprequest.cc index b34da086f..31a890f63 100644 --- a/server/core/httprequest.cc +++ b/server/core/httprequest.cc @@ -82,186 +82,13 @@ static void process_uri(string& uri, deque& uri_parts) } } -static bool process_options(string& uri, map& 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& 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 options; - - /** Process request options */ - if (!process_options(uri, options)) - { - return NULL; - } - - /** Split the URI into separate parts */ - deque 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 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() diff --git a/server/core/httpresponse.cc b/server/core/httpresponse.cc index 68d9c5d36..264138cc8 100644 --- a/server/core/httpresponse.cc +++ b/server/core/httpresponse.cc @@ -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& 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::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; } diff --git a/server/core/maxscale/admin.hh b/server/core/maxscale/admin.hh index dbce8d543..73a9f35e8 100644 --- a/server/core/maxscale/admin.hh +++ b/server/core/maxscale/admin.hh @@ -20,57 +20,6 @@ #include #include "http.hh" -#include "adminclient.hh" - -using std::deque; -using std::string; - -typedef deque 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(); diff --git a/server/core/maxscale/adminclient.hh b/server/core/maxscale/adminclient.hh deleted file mode 100644 index 8f06ee714..000000000 --- a/server/core/maxscale/adminclient.hh +++ /dev/null @@ -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 - -#include -#include -#include - -#include -#include - -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 SAdminClient; diff --git a/server/core/maxscale/http.hh b/server/core/maxscale/http.hh index 4f8b9d1d4..da77777a3 100644 --- a/server/core/maxscale/http.hh +++ b/server/core/maxscale/http.hh @@ -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 * diff --git a/server/core/maxscale/httprequest.hh b/server/core/maxscale/httprequest.hh index 8fc414bb3..0a1c87418 100644 --- a/server/core/maxscale/httprequest.hh +++ b/server/core/maxscale/httprequest.hh @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,22 @@ class HttpRequest; /** Typedef for managed pointer */ typedef std::shared_ptr SHttpRequest; +static int value_iterator(void *cls, + enum MHD_ValueKind kind, + const char *key, + const char *value) +{ + std::pair* cmp = (std::pair*)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::const_iterator it = m_headers.find(header); + std::pair 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::const_iterator it = m_options.find(option); + std::pair 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 m_headers; /**< Request headers */ map m_options; /**< Request options */ Closer m_json; /**< Request body */ string m_json_string; /**< String version of @c m_json */ string m_resource; /**< Requested resource */ deque 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; }; diff --git a/server/core/maxscale/httpresponse.hh b/server/core/maxscale/httpresponse.hh index 9a0d71ccf..4f42655e7 100644 --- a/server/core/maxscale/httpresponse.hh +++ b/server/core/maxscale/httpresponse.hh @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -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& 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 m_headers; /**< Message headers */ - enum http_code m_code; /**< The HTTP code for the response */ + int m_code; /**< The HTTP code for the response */ }; diff --git a/server/core/maxscale/resource.hh b/server/core/maxscale/resource.hh index d7fe6fd24..ba0a47669 100644 --- a/server/core/maxscale/resource.hh +++ b/server/core/maxscale/resource.hh @@ -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); }; /** diff --git a/server/core/resource.cc b/server/core/resource.cc index 10ec51a5a..59ef81bda 100644 --- a/server/core/resource.cc +++ b/server/core/resource.cc @@ -50,7 +50,7 @@ protected: { // TODO: Generate this via the inter-thread messaging system Closer 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 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 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 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 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 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 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 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 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); } }; diff --git a/server/core/test/CMakeLists.txt b/server/core/test/CMakeLists.txt index 3cced39c8..89ca4cc0a 100644 --- a/server/core/test/CMakeLists.txt +++ b/server/core/test/CMakeLists.txt @@ -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 diff --git a/server/core/test/testhttp.cc b/server/core/test/testhttp.cc deleted file mode 100644 index 1fa68cbcc..000000000 --- a/server/core/test/testhttp.cc +++ /dev/null @@ -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 - -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!", - "

I am a paragraph

", - "", - 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; -}