MXS-1220: Add option parsing

The options of a request are now parsed and exposed by the HttpRequest
class.

Added tests for the request options.

Also added missing error handling of invalid requests.
This commit is contained in:
Markus Mäkelä
2017-04-17 04:04:59 +03:00
committed by Markus Mäkelä
parent 9d0d394361
commit 34ee4a1997
3 changed files with 167 additions and 24 deletions

View File

@ -14,6 +14,7 @@
#include "maxscale/httprequest.hh" #include "maxscale/httprequest.hh"
#include <ctype.h> #include <ctype.h>
#include <string.h>
/** TODO: Move this to a C++ string utility header */ /** TODO: Move this to a C++ string utility header */
namespace maxscale namespace maxscale
@ -52,17 +53,68 @@ static inline string& trim(string& str)
HttpRequest* HttpRequest::parse(string data) HttpRequest* HttpRequest::parse(string data)
{ {
size_t pos = data.find("\r\n"); size_t pos = data.find("\r\n");
if (pos == string::npos)
{
return NULL;
}
string request_line = data.substr(0, pos); string request_line = data.substr(0, pos);
data.erase(0, pos + 2); data.erase(0, pos + 2);
pos = request_line.find(" "); /** Request method */
if ((pos = request_line.find(" ")) == string::npos)
{
return NULL;
}
string verb = request_line.substr(0, pos); string verb = request_line.substr(0, pos);
request_line.erase(0, pos + 1); request_line.erase(0, pos + 1);
pos = request_line.find(" "); /** Get the combined URL/option string */
if ((pos = request_line.find(" ")) == string::npos)
{
return NULL;
}
string uri = request_line.substr(0, pos); string uri = request_line.substr(0, pos);
request_line.erase(0, pos + 1); request_line.erase(0, pos + 1);
/** Process request options */
pos = uri.find("?");
map<string, string> options;
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 - 1);
string value = opt.substr(pos + 1);
options[key] = value;
}
else
{
/** Invalid option */
return NULL;
}
tok = strtok_r(NULL, ",", &saved);
}
}
pos = request_line.find("\r\n"); pos = request_line.find("\r\n");
string http_version = request_line.substr(0, pos); string http_version = request_line.substr(0, pos);
request_line.erase(0, pos + 2); request_line.erase(0, pos + 2);
@ -86,10 +138,18 @@ HttpRequest* HttpRequest::parse(string data)
header_line.erase(0, pos + 1); header_line.erase(0, pos + 1);
headers[key] = mxs::trim(header_line); headers[key] = mxs::trim(header_line);
} }
else
{
/** Invalid header */
return NULL;
}
} }
/** The headers are now processed and consumed. The message body is /**
* the only thing left in the request string. */ * 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; bool ok = false;
HttpRequest* request = NULL; HttpRequest* request = NULL;
@ -120,11 +180,12 @@ HttpRequest* HttpRequest::parse(string data)
if (ok) if (ok)
{ {
request = new HttpRequest(); request = new HttpRequest();
request->m_verb = verb_value;
request->m_resource = uri;
request->m_headers = headers; request->m_headers = headers;
request->m_options = options;
request->m_json.reset(body); request->m_json.reset(body);
request->m_json_string = data; request->m_json_string = data;
request->m_resource = uri;
request->m_verb = verb_value;
} }
return request; return request;

View File

@ -74,12 +74,32 @@ public:
* *
* @param header Header to get * @param header Header to get
* *
* @return String value or empty string if no header found * @return Header value or empty string if the header was not found
*/ */
const string get_header(const string header) string get_header(const string header) const
{ {
string rval; string rval;
map<string, string>::iterator it = m_headers.find(header); map<string, string>::const_iterator it = m_headers.find(header);
if (it != m_headers.end())
{
rval = it->second;
}
return rval;
}
/**
* @brief Get option value
*
* @param header Option to get
*
* @return Option value or empty string if the option was not found
*/
string get_option(const string option) const
{
string rval;
map<string, string>::const_iterator it = m_options.find(option);
if (it != m_headers.end()) if (it != m_headers.end())
{ {
@ -125,6 +145,7 @@ private:
HttpRequest& operator = (const HttpRequest&); HttpRequest& operator = (const HttpRequest&);
map<string, string> m_headers; /**< Request headers */ map<string, string> m_headers; /**< Request headers */
map<string, string> m_options; /**< Request options */
Closer<json_t*> m_json; /**< Request body */ Closer<json_t*> m_json; /**< Request body */
string m_json_string; /**< String version of @c m_json */ string m_json_string; /**< String version of @c m_json */
string m_resource; /**< Requested resource */ string m_resource; /**< Requested resource */

View File

@ -42,18 +42,22 @@ const char* verbs_fail[] =
NULL NULL
}; };
const char* paths_pass[] = static struct
{ {
"/", const char* input;
"*", const char* output;
"/test/", } paths_pass[] =
"/test", {
"/servers/list", { "/", "/" },
"/servers/list/", { "*", "*" },
"/?test=true", { "/test/", "/test/" },
"/test/?test=y", { "/test", "/test" },
"/?", { "/servers/list", "/servers/list" },
NULL { "/servers/list/", "/servers/list/" },
{ "/?test=true", "/" },
{ "/test/?test=y", "/test/" },
{ "/?", "/" },
{}
}; };
const char* paths_fail[] = const char* paths_fail[] =
@ -84,17 +88,17 @@ int test_basic()
/** Test parts that should pass */ /** Test parts that should pass */
for (int i = 0; verbs_pass[i]; i++) for (int i = 0; verbs_pass[i]; i++)
{ {
for (int j = 0; paths_pass[j]; j++) for (int j = 0; paths_pass[j].input; j++)
{ {
for (int k = 0; proto_pass[k]; k++) for (int k = 0; proto_pass[k]; k++)
{ {
stringstream ss; stringstream ss;
ss << verbs_pass[i] << " " << paths_pass[j] << " " << proto_pass[k] << "\r\n\r\n"; ss << verbs_pass[i] << " " << paths_pass[j].input << " " << proto_pass[k] << "\r\n\r\n";
SHttpRequest parser(HttpRequest::parse(ss.str())); SHttpRequest parser(HttpRequest::parse(ss.str()));
TEST(parser.get() != NULL, "Valid HTTP request should be parsed: %s", ss.str().c_str()); TEST(parser.get() != NULL, "Valid HTTP request should be parsed: %s", ss.str().c_str());
TEST(parser->get_resource() == string(paths_pass[j]), TEST(parser->get_resource() == string(paths_pass[j].output),
"The request path '%s' should be correct: %s", "The request path '%s' should be correct: %s",
paths_pass[j], parser->get_resource().c_str()); paths_pass[j].output, parser->get_resource().c_str());
} }
} }
} }
@ -307,6 +311,63 @@ int test_message_body()
return 0; 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() int test_response()
{ {
TEST(HttpResponse().get_response().find("200 OK") != string::npos, TEST(HttpResponse().get_response().find("200 OK") != string::npos,