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:

committed by
Markus Mäkelä

parent
4eb121ce35
commit
c937457738
@ -84,3 +84,25 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
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
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
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.
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
# JANSSON_INCLUDE_DIR - Path to Jansson headers
|
# JANSSON_INCLUDE_DIR - Path to Jansson headers
|
||||||
|
|
||||||
find_path(JANSSON_INCLUDE_DIR jansson.h)
|
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)
|
if (JANSSON_INCLUDE_DIR AND JANSSON_LIBRARIES)
|
||||||
message(STATUS "Found Jansson: ${JANSSON_LIBRARIES}")
|
message(STATUS "Found Jansson: ${JANSSON_LIBRARIES}")
|
||||||
|
@ -56,7 +56,23 @@ elseif(WITH_TCMALLOC)
|
|||||||
target_link_libraries(maxscale-common ${TCMALLOC_LIBRARIES})
|
target_link_libraries(maxscale-common ${TCMALLOC_LIBRARIES})
|
||||||
endif()
|
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)
|
add_dependencies(maxscale-common pcre2 connector-c)
|
||||||
set_target_properties(maxscale-common PROPERTIES VERSION "1.0.0")
|
set_target_properties(maxscale-common PROPERTIES VERSION "1.0.0")
|
||||||
|
@ -99,7 +99,9 @@ void AdminClient::process()
|
|||||||
enum http_code status = parser.get() ? HTTP_200_OK : HTTP_400_BAD_REQUEST;
|
enum http_code status = parser.get() ? HTTP_200_OK : HTTP_400_BAD_REQUEST;
|
||||||
|
|
||||||
atomic_write_int64(&m_last_activity, hkheartbeat);
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -20,14 +20,29 @@ namespace maxscale
|
|||||||
{
|
{
|
||||||
static inline string& trim(string& str)
|
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()))
|
while (it != str.end() && isspace(*it))
|
||||||
{
|
{
|
||||||
str.erase(str.rbegin().base());
|
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;
|
return str;
|
||||||
@ -76,22 +91,48 @@ HttpRequest* HttpRequest::parse(string data)
|
|||||||
/** The headers are now processed and consumed. The message body is
|
/** The headers are now processed and consumed. The message body is
|
||||||
* the only thing left in the request string. */
|
* the only thing left in the request string. */
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
HttpRequest* request = NULL;
|
HttpRequest* request = NULL;
|
||||||
enum http_verb verb_value = string_to_http_verb(verb);
|
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 (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 = new HttpRequest();
|
||||||
request->m_verb = verb_value;
|
request->m_verb = verb_value;
|
||||||
request->m_resource = uri;
|
request->m_resource = uri;
|
||||||
request->m_headers = headers;
|
request->m_headers = headers;
|
||||||
request->m_body = data;
|
request->m_json.reset(body);
|
||||||
|
request->m_json_string = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRequest::HttpRequest()
|
HttpRequest::HttpRequest():
|
||||||
|
m_json(NULL),
|
||||||
|
m_verb(HTTP_UNKNOWN)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,15 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <tr1/memory>
|
#include <tr1/memory>
|
||||||
|
|
||||||
|
#include <maxscale/jansson.hh>
|
||||||
|
#include <maxscale/utils.hh>
|
||||||
|
|
||||||
#include "http.hh"
|
#include "http.hh"
|
||||||
|
|
||||||
using std::shared_ptr;
|
using std::shared_ptr;
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::map;
|
using std::map;
|
||||||
|
using mxs::Closer;
|
||||||
|
|
||||||
class HttpRequest;
|
class HttpRequest;
|
||||||
|
|
||||||
@ -85,24 +89,24 @@ public:
|
|||||||
return rval;
|
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
|
* @brief Return request body
|
||||||
*
|
*
|
||||||
* @return Request body or empty string if no body is defined
|
* @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&);
|
HttpRequest& operator = (const HttpRequest&);
|
||||||
|
|
||||||
map<string, string> m_headers;
|
map<string, string> m_headers;
|
||||||
string m_body;
|
Closer<json_t*> m_json;
|
||||||
|
string m_json_string;
|
||||||
string m_resource;
|
string m_resource;
|
||||||
enum http_verb m_verb;
|
enum http_verb m_verb;
|
||||||
};
|
};
|
||||||
|
@ -159,12 +159,144 @@ int test_headers()
|
|||||||
return 0;
|
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 main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
rc += test_basic();
|
rc += test_basic();
|
||||||
rc += test_headers();
|
rc += test_headers();
|
||||||
|
rc += test_message_body();
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user