diff options
author | Ed Tanous <ed@tanous.net> | 2020-07-21 18:46:25 +0300 |
---|---|---|
committer | Ed Tanous <ed@tanous.net> | 2021-12-20 04:00:35 +0300 |
commit | af4edf686e684d728fccbb69a8f550fd2adab46a (patch) | |
tree | 1c97b4f7b75a310105ab7ba86fdbf100117c3782 | |
parent | 47c9e106e0057dd70133d50e928e48cbc68e709a (diff) | |
download | bmcweb-af4edf686e684d728fccbb69a8f550fd2adab46a.tar.xz |
Implement MIME parsing
This commit adds two core features to bmcweb:
1. A multipart mime parser that can read multipart form requests into
bmcweb. This is implemented as a generic parser that identifies the
content-type strings and parses them into structures.
2. A /login route that can be logged into with a multipart form. This
is to allow changing the login screen to a purely forms based
implementation, thus removing the very large whitelist we currently have
to maintain, and removing javascript from our threat envelope.
More testing is still needed, as this is a parser that exists outside of
the secured areas, but in this simple example, it seems to work well.
Tested: curl -vvvvv --insecure -X POST -F 'username=root' -F
'password=0penBmc' https://<bmc ip address>:18080/login
Returned; { "data": "User 'root' logged in", "message": "200 OK",
"status": "ok" }
Change-Id: Icc3f4c082d584170b65b9e82f7876926cd38035d
Signed-off-by: Ed Tanous<ed@tanous.net>
Signed-off-by: George Liu <liuxiwei@inspur.com>
-rw-r--r-- | include/login_routes.hpp | 46 | ||||
-rw-r--r-- | include/multipart_parser.hpp | 338 | ||||
-rw-r--r-- | include/ut/multipart_test.cpp | 237 | ||||
-rw-r--r-- | meson.build | 1 |
4 files changed, 622 insertions, 0 deletions
diff --git a/include/login_routes.hpp b/include/login_routes.hpp index 881035619a..858968bfd9 100644 --- a/include/login_routes.hpp +++ b/include/login_routes.hpp @@ -1,5 +1,7 @@ #pragma once +#include "multipart_parser.hpp" + #include <app.hpp> #include <boost/container/flat_set.hpp> #include <common.hpp> @@ -121,6 +123,50 @@ inline void requestRoutes(App& app) } } } + else if (boost::starts_with(contentType, "multipart/form-data")) + { + looksLikePhosphorRest = true; + MultipartParser parser; + ParserError ec = parser.parse(req); + if (ec != ParserError::PARSER_SUCCESS) + { + // handle error + BMCWEB_LOG_ERROR << "MIME parse failed, ec : " + << static_cast<int>(ec); + asyncResp->res.result( + boost::beast::http::status::bad_request); + return; + } + + for (const FormPart& formpart : parser.mime_fields) + { + boost::beast::http::fields::const_iterator it = + formpart.fields.find("Content-Disposition"); + if (it == formpart.fields.end()) + { + BMCWEB_LOG_ERROR << "Couldn't find Content-Disposition"; + asyncResp->res.result( + boost::beast::http::status::bad_request); + continue; + } + + BMCWEB_LOG_INFO << "Parsing value " << it->value(); + + if (it->value() == "form-data; name=\"username\"") + { + username = formpart.content; + } + else if (it->value() == "form-data; name=\"password\"") + { + password = formpart.content; + } + else + { + BMCWEB_LOG_INFO << "Extra format, ignore it." + << it->value(); + } + } + } else { // check if auth was provided as a headers diff --git a/include/multipart_parser.hpp b/include/multipart_parser.hpp new file mode 100644 index 0000000000..3728311fbe --- /dev/null +++ b/include/multipart_parser.hpp @@ -0,0 +1,338 @@ +#pragma once + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/beast/http/fields.hpp> +#include <http_request.hpp> + +#include <string> +#include <string_view> + +enum class ParserError +{ + PARSER_SUCCESS, + ERROR_BOUNDARY_FORMAT, + ERROR_BOUNDARY_CR, + ERROR_BOUNDARY_LF, + ERROR_BOUNDARY_DATA, + ERROR_EMPTY_HEADER, + ERROR_HEADER_NAME, + ERROR_HEADER_VALUE, + ERROR_HEADER_ENDING +}; + +enum class State +{ + START, + START_BOUNDARY, + HEADER_FIELD_START, + HEADER_FIELD, + HEADER_VALUE_START, + HEADER_VALUE, + HEADER_VALUE_ALMOST_DONE, + HEADERS_ALMOST_DONE, + PART_DATA_START, + PART_DATA, + END +}; + +enum class Boundary +{ + NON_BOUNDARY, + PART_BOUNDARY, + END_BOUNDARY, +}; + +struct FormPart +{ + boost::beast::http::fields fields; + std::string content; +}; + +class MultipartParser +{ + public: + MultipartParser() = default; + + [[nodiscard]] ParserError parse(const crow::Request& req) + { + std::string_view contentType = req.getHeaderValue("content-type"); + + const std::string boundaryFormat = "multipart/form-data; boundary="; + if (!boost::starts_with(req.getHeaderValue("content-type"), + boundaryFormat)) + { + return ParserError::ERROR_BOUNDARY_FORMAT; + } + + std::string_view ctBoundary = contentType.substr(boundaryFormat.size()); + + boundary = "\r\n--"; + boundary += ctBoundary; + indexBoundary(); + lookbehind.resize(boundary.size() + 8); + state = State::START; + + const char* buffer = req.body.data(); + size_t len = req.body.size(); + size_t prevIndex = index; + char cl = 0; + + for (size_t i = 0; i < len; i++) + { + char c = buffer[i]; + switch (state) + { + case State::START: + index = 0; + state = State::START_BOUNDARY; + [[fallthrough]]; + case State::START_BOUNDARY: + if (index == boundary.size() - 2) + { + if (c != cr) + { + return ParserError::ERROR_BOUNDARY_CR; + } + index++; + break; + } + else if (index - 1 == boundary.size() - 2) + { + if (c != lf) + { + return ParserError::ERROR_BOUNDARY_LF; + } + index = 0; + mime_fields.push_back({}); + state = State::HEADER_FIELD_START; + break; + } + if (c != boundary[index + 2]) + { + return ParserError::ERROR_BOUNDARY_DATA; + } + index++; + break; + case State::HEADER_FIELD_START: + currentHeaderName.resize(0); + state = State::HEADER_FIELD; + headerFieldMark = i; + index = 0; + [[fallthrough]]; + case State::HEADER_FIELD: + if (c == cr) + { + headerFieldMark = 0; + state = State::HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c == hyphen) + { + break; + } + + if (c == colon) + { + if (index == 1) + { + return ParserError::ERROR_EMPTY_HEADER; + } + currentHeaderName.append(buffer + headerFieldMark, + i - headerFieldMark); + state = State::HEADER_VALUE_START; + break; + } + cl = lower(c); + if (cl < 'a' || cl > 'z') + { + return ParserError::ERROR_HEADER_NAME; + } + break; + case State::HEADER_VALUE_START: + if (c == space) + { + break; + } + headerValueMark = i; + state = State::HEADER_VALUE; + [[fallthrough]]; + case State::HEADER_VALUE: + if (c == cr) + { + std::string_view value(buffer + headerValueMark, + i - headerValueMark); + mime_fields.rbegin()->fields.set(currentHeaderName, + value); + state = State::HEADER_VALUE_ALMOST_DONE; + } + break; + case State::HEADER_VALUE_ALMOST_DONE: + if (c != lf) + { + return ParserError::ERROR_HEADER_VALUE; + } + state = State::HEADER_FIELD_START; + break; + case State::HEADERS_ALMOST_DONE: + if (c != lf) + { + return ParserError::ERROR_HEADER_ENDING; + } + state = State::PART_DATA_START; + break; + case State::PART_DATA_START: + state = State::PART_DATA; + partDataMark = i; + [[fallthrough]]; + case State::PART_DATA: + if (index == 0) + { + skipNonBoundary(buffer, len, boundary.size() - 1, i); + c = buffer[i]; + } + processPartData(prevIndex, index, buffer, i, c, state); + break; + case State::END: + break; + } + } + return ParserError::PARSER_SUCCESS; + } + std::vector<FormPart> mime_fields; + std::string boundary; + + private: + void indexBoundary() + { + std::fill(boundaryIndex.begin(), boundaryIndex.end(), 0); + for (const char current : boundary) + { + boundaryIndex[static_cast<unsigned char>(current)] = true; + } + } + + char lower(char c) const + { + return static_cast<char>(c | 0x20); + } + + inline bool isBoundaryChar(char c) const + { + return boundaryIndex[static_cast<unsigned char>(c)]; + } + + void skipNonBoundary(const char* buffer, size_t len, size_t boundaryEnd, + size_t& i) + { + // boyer-moore derived algorithm to safely skip non-boundary data + while (i + boundary.size() <= len) + { + if (isBoundaryChar(buffer[i + boundaryEnd])) + { + break; + } + i += boundary.size(); + } + } + + void processPartData(size_t& prevIndex, size_t& index, const char* buffer, + size_t& i, char c, State& state) + { + prevIndex = index; + + if (index < boundary.size()) + { + if (boundary[index] == c) + { + if (index == 0) + { + mime_fields.rbegin()->content += std::string_view( + buffer + partDataMark, i - partDataMark); + } + index++; + } + else + { + index = 0; + } + } + else if (index == boundary.size()) + { + index++; + if (c == cr) + { + // cr = part boundary + flags = Boundary::PART_BOUNDARY; + } + else if (c == hyphen) + { + // hyphen = end boundary + flags = Boundary::END_BOUNDARY; + } + else + { + index = 0; + } + } + else + { + if (flags == Boundary::PART_BOUNDARY) + { + index = 0; + if (c == lf) + { + // unset the PART_BOUNDARY flag + flags = Boundary::NON_BOUNDARY; + mime_fields.push_back({}); + state = State::HEADER_FIELD_START; + return; + } + } + if (flags == Boundary::END_BOUNDARY) + { + if (c == hyphen) + { + state = State::END; + } + } + } + + if (index > 0) + { + lookbehind[index - 1] = c; + } + else if (prevIndex > 0) + { + // if our boundary turned out to be rubbish, the captured + // lookbehind belongs to partData + + mime_fields.rbegin()->content += lookbehind.substr(0, prevIndex); + prevIndex = 0; + partDataMark = i; + + // reconsider the current character even so it interrupted + // the sequence it could be the beginning of a new sequence + i--; + } + } + + std::string currentHeaderName; + std::string currentHeaderValue; + + static constexpr char cr = '\r'; + static constexpr char lf = '\n'; + static constexpr char space = ' '; + static constexpr char hyphen = '-'; + static constexpr char colon = ':'; + + std::array<bool, 256> boundaryIndex; + std::string lookbehind; + State state; + Boundary flags; + size_t index = 0; + size_t partDataMark = 0; + size_t headerFieldMark = 0; + size_t headerValueMark = 0; +}; diff --git a/include/ut/multipart_test.cpp b/include/ut/multipart_test.cpp new file mode 100644 index 0000000000..35e05e202f --- /dev/null +++ b/include/ut/multipart_test.cpp @@ -0,0 +1,237 @@ +#include <http_utility.hpp> +#include <multipart_parser.hpp> + +#include <map> + +#include "gmock/gmock.h" + +class MultipartTest : public ::testing::Test +{ + public: + boost::beast::http::request<boost::beast::http::string_body> req{}; + MultipartParser parser; + std::error_code ec; +}; + +TEST_F(MultipartTest, TestGoodMultipartParser) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "111111111111111111111111112222222222222222222222222222222\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "{\r\n-----------------------------d74496d66958873e123456\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test3\"\r\n\r\n" + "{\r\n--------d74496d6695887}\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ParserError rc = parser.parse(reqIn); + ASSERT_EQ(rc, ParserError::PARSER_SUCCESS); + + ASSERT_EQ(parser.boundary, + "\r\n-----------------------------d74496d66958873e"); + ASSERT_EQ(parser.mime_fields.size(), 3); + + ASSERT_EQ(parser.mime_fields[0].fields.at("Content-Disposition"), + "form-data; name=\"Test1\""); + ASSERT_EQ(parser.mime_fields[0].content, + "111111111111111111111111112222222222222222222222222222222"); + + ASSERT_EQ(parser.mime_fields[1].fields.at("Content-Disposition"), + "form-data; name=\"Test2\""); + ASSERT_EQ(parser.mime_fields[1].content, + "{\r\n-----------------------------d74496d66958873e123456"); + ASSERT_EQ(parser.mime_fields[2].fields.at("Content-Disposition"), + "form-data; name=\"Test3\""); + ASSERT_EQ(parser.mime_fields[2].content, "{\r\n--------d74496d6695887}"); +} + +TEST_F(MultipartTest, TestBadMultipartParser1) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "1234567890\r\n" + "-----------------------------d74496d66958873e\r-\r\n"; + + crow::Request reqIn(req, ec); + ParserError rc = parser.parse(reqIn); + ASSERT_EQ(rc, ParserError::PARSER_SUCCESS); + + ASSERT_EQ(parser.boundary, + "\r\n-----------------------------d74496d66958873e"); + ASSERT_EQ(parser.mime_fields.size(), 1); +} + +TEST_F(MultipartTest, TestBadMultipartParser2) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "abcd\r\n" + "-----------------------------d74496d66958873e-\r\n"; + + crow::Request reqIn(req, ec); + ParserError rc = parser.parse(reqIn); + ASSERT_EQ(rc, ParserError::PARSER_SUCCESS); + + ASSERT_EQ(parser.boundary, + "\r\n-----------------------------d74496d66958873e"); + ASSERT_EQ(parser.mime_fields.size(), 1); +} + +TEST_F(MultipartTest, TestErrorBoundaryFormat) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary+=-----------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "{\"Key1\": 11223333333333333333333333333333333333333333}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_BOUNDARY_FORMAT); +} + +TEST_F(MultipartTest, TestErrorBoundaryCR) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_BOUNDARY_CR); +} + +TEST_F(MultipartTest, TestErrorBoundaryLF) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_BOUNDARY_LF); +} + +TEST_F(MultipartTest, TestErrorBoundaryData) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d7449sd6d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r\n" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_BOUNDARY_DATA); +} + +TEST_F(MultipartTest, TestErrorEmptyHeader) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + ": form-data; name=\"Test1\"\r\n" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_EMPTY_HEADER); +} + +TEST_F(MultipartTest, TestErrorHeaderName) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-!!Disposition: form-data; name=\"Test1\"\r\n" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_HEADER_NAME); +} + +TEST_F(MultipartTest, TestErrorHeaderValue) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_HEADER_VALUE); +} + +TEST_F(MultipartTest, TestErrorHeaderEnding) +{ + req.set("Content-Type", + "multipart/form-data; " + "boundary=---------------------------d74496d66958873e"); + + req.body() = "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test1\"\r\n\r" + "{\"Key1\": 112233}\r\n" + "-----------------------------d74496d66958873e\r\n" + "Content-Disposition: form-data; name=\"Test2\"\r\n\r\n" + "123456\r\n" + "-----------------------------d74496d66958873e--\r\n"; + + crow::Request reqIn(req, ec); + ASSERT_EQ(parser.parse(reqIn), ParserError::ERROR_HEADER_ENDING); +} diff --git a/meson.build b/meson.build index bf34d5e736..c9066d4967 100644 --- a/meson.build +++ b/meson.build @@ -375,6 +375,7 @@ executable( srcfiles_unittest = [ 'http/ut/utility_test.cpp', 'include/ut/dbus_utility_test.cpp', + 'include/ut/multipart_test.cpp', 'include/ut/http_utility_test.cpp', 'include/ut/human_sort_test.cpp', 'redfish-core/ut/configfile_test.cpp', |