diff --git a/LICENSE-THIRDPARTY.TXT b/LICENSE-THIRDPARTY.TXT index 45f5071f5..a56be2180 100644 --- a/LICENSE-THIRDPARTY.TXT +++ b/LICENSE-THIRDPARTY.TXT @@ -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 + +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. diff --git a/cmake/FindJansson.cmake b/cmake/FindJansson.cmake index 8c2216cac..67ad57525 100644 --- a/cmake/FindJansson.cmake +++ b/cmake/FindJansson.cmake @@ -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}") diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt index 9bf853d82..98574c55a 100644 --- a/server/core/CMakeLists.txt +++ b/server/core/CMakeLists.txt @@ -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") diff --git a/server/core/adminclient.cc b/server/core/adminclient.cc index 348659d86..418e3de97 100644 --- a/server/core/adminclient.cc +++ b/server/core/adminclient.cc @@ -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 { diff --git a/server/core/httprequest.cc b/server/core/httprequest.cc index 50ec69e97..f060c7298 100644 --- a/server/core/httprequest.cc +++ b/server/core/httprequest.cc @@ -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) { } diff --git a/server/core/maxscale/httprequest.hh b/server/core/maxscale/httprequest.hh index ebcf08f46..ad273509f 100644 --- a/server/core/maxscale/httprequest.hh +++ b/server/core/maxscale/httprequest.hh @@ -18,11 +18,15 @@ #include #include +#include +#include + #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 m_headers; - string m_body; + Closer m_json; + string m_json_string; string m_resource; enum http_verb m_verb; }; diff --git a/server/core/test/testhttp.cc b/server/core/test/testhttp.cc index 69c9ee469..8a311ba5c 100644 --- a/server/core/test/testhttp.cc +++ b/server/core/test/testhttp.cc @@ -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!", + "

I am a paragraph

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