MXS-1220: Add request body parsing

The HTTP request body is expected to be a valid JSON object. All other
requests are considered malformed requests and result in a HTTP 400 error.

Added the Jansson license to the LICENSE-THIRDPARTY.TXT file. Imported
some of the tests from the Jansson test suite to the HttpParser test.
This commit is contained in:
Markus Mäkelä 2017-04-16 21:31:52 +03:00 committed by Markus Mäkelä
parent 4eb121ce35
commit c937457738
7 changed files with 244 additions and 24 deletions

View File

@ -84,3 +84,25 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
The Jansson JSON parsing library is distributed under the MIT license.
Copyright (c) 2009-2016 Petri Lehtinen <petri@digip.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -6,7 +6,9 @@
# JANSSON_INCLUDE_DIR - Path to Jansson headers
find_path(JANSSON_INCLUDE_DIR jansson.h)
find_library(JANSSON_LIBRARIES NAMES libjansson.so libjansson.a)
# Use the static library
find_library(JANSSON_LIBRARIES NAMES libjansson.a)
if (JANSSON_INCLUDE_DIR AND JANSSON_LIBRARIES)
message(STATUS "Found Jansson: ${JANSSON_LIBRARIES}")

View File

@ -56,7 +56,23 @@ elseif(WITH_TCMALLOC)
target_link_libraries(maxscale-common ${TCMALLOC_LIBRARIES})
endif()
target_link_libraries(maxscale-common ${MARIADB_CONNECTOR_LIBRARIES} ${LZMA_LINK_FLAGS} ${PCRE2_LIBRARIES} ${CURL_LIBRARIES} ssl pthread crypt dl crypto inih z rt m stdc++)
target_link_libraries(maxscale-common
${MARIADB_CONNECTOR_LIBRARIES}
${LZMA_LINK_FLAGS}
${PCRE2_LIBRARIES}
${CURL_LIBRARIES}
${JANSSON_LIBRARIES}
ssl
pthread
crypt
dl
crypto
inih
z
rt
m
stdc++
)
add_dependencies(maxscale-common pcre2 connector-c)
set_target_properties(maxscale-common PROPERTIES VERSION "1.0.0")

View File

@ -99,7 +99,9 @@ void AdminClient::process()
enum http_code status = parser.get() ? HTTP_200_OK : HTTP_400_BAD_REQUEST;
atomic_write_int64(&m_last_activity, hkheartbeat);
write_response(m_fd, HTTP_200_OK, parser->get_body());
/** Echo the request body back */
write_response(m_fd, HTTP_200_OK, parser->get_json_str());
}
else
{

View File

@ -20,14 +20,29 @@ namespace maxscale
{
static inline string& trim(string& str)
{
while (isspace(*str.begin()))
if (str.length())
{
str.erase(str.begin());
}
if (isspace(*str.begin()))
{
string::iterator it = str.begin();
while (isspace(*str.rbegin()))
{
str.erase(str.rbegin().base());
while (it != str.end() && isspace(*it))
{
it++;
}
str.erase(str.begin(), it);
}
if (isspace(*str.rbegin()))
{
string::reverse_iterator it = str.rbegin();
while (it != str.rend() && isspace(*it))
{
it++;
}
str.erase(it.base(), str.end());
}
}
return str;
@ -76,22 +91,48 @@ HttpRequest* HttpRequest::parse(string data)
/** The headers are now processed and consumed. The message body is
* the only thing left in the request string. */
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_verb = verb_value;
request->m_resource = uri;
request->m_headers = headers;
request->m_body = data;
request->m_json.reset(body);
request->m_json_string = data;
}
return request;
}
HttpRequest::HttpRequest()
HttpRequest::HttpRequest():
m_json(NULL),
m_verb(HTTP_UNKNOWN)
{
}

View File

@ -18,11 +18,15 @@
#include <map>
#include <tr1/memory>
#include <maxscale/jansson.hh>
#include <maxscale/utils.hh>
#include "http.hh"
using std::shared_ptr;
using std::string;
using std::map;
using mxs::Closer;
class HttpRequest;
@ -85,24 +89,24 @@ public:
return rval;
}
/**
* @brief Check if body is defined
*
* @return True if body is defined
*/
bool have_body() const
{
return m_body.length() != 0;
}
/**
* @brief Return request body
*
* @return Request body or empty string if no body is defined
*/
const string& get_body() const
const string& get_json_str() const
{
return m_body;
return m_json_string;
}
/**
* @brief Return raw JSON body
*
* @return Raw JSON body or NULL if no body is defined
*/
const json_t* get_json() const
{
return m_json.get();
}
/**
@ -121,7 +125,8 @@ private:
HttpRequest& operator = (const HttpRequest&);
map<string, string> m_headers;
string m_body;
Closer<json_t*> m_json;
string m_json_string;
string m_resource;
enum http_verb m_verb;
};

View File

@ -159,12 +159,144 @@ int test_headers()
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
};
int test_message_body()
{
for (int i = 0; body_pass[i]; i++)
{
stringstream ss;
ss << "GET / 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++)
{
stringstream ss;
ss << "GET / 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;
}
int main(int argc, char** argv)
{
int rc = 0;
rc += test_basic();
rc += test_headers();
rc += test_message_body();
return rc;
}