Fix a bug when parsing multipart/form-data (#1358)

This commit is contained in:
An Tao 2022-08-25 23:04:23 +08:00 committed by GitHub
parent f582a16adb
commit c40bb2bc1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 271 additions and 98 deletions

View File

@ -33,34 +33,34 @@ class HttpFileImpl;
class DROGON_EXPORT HttpFile
{
public:
HttpFile(std::shared_ptr<HttpFileImpl> &&implPtr);
explicit HttpFile(std::shared_ptr<HttpFileImpl> &&implPtr) noexcept;
/// Return the file name;
const std::string &getFileName() const;
const std::string &getFileName() const noexcept;
/// Return the file extension;
/// Note: After the HttpFile object is destroyed, do not use this
/// string_view object.
string_view getFileExtension() const;
string_view getFileExtension() const noexcept;
/// Return the name of the item in multiple parts.
const std::string &getItemName() const;
const std::string &getItemName() const noexcept;
/// Return the type of file.
FileType getFileType() const;
FileType getFileType() const noexcept;
/// Set the file name, usually called by the MultiPartParser parser.
void setFileName(const std::string &fileName);
void setFileName(const std::string &fileName) noexcept;
/// Set the contents of the file, usually called by the MultiPartParser
/// parser.
void setFile(const char *data, size_t length);
void setFile(const char *data, size_t length) noexcept;
/// Save the file to the file system.
/**
* The folder saving the file is app().getUploadPath().
* The full path is app().getUploadPath()+"/"+this->getFileName()
*/
int save() const;
int save() const noexcept;
/// Save the file to @param path
/**
@ -69,7 +69,7 @@ class DROGON_EXPORT HttpFile
* otherwise the file is saved as
* app().getUploadPath()+"/"+path+"/"+this->getFileName()
*/
int save(const std::string &path) const;
int save(const std::string &path) const noexcept;
/// Save the file to file system with a new name
/**
@ -77,14 +77,14 @@ class DROGON_EXPORT HttpFile
* the full path is app().getUploadPath()+"/"+filename, otherwise the file
* is saved as the filename
*/
int saveAs(const std::string &fileName) const;
int saveAs(const std::string &fileName) const noexcept;
/**
* @brief return the content of the file.
*
* @return string_view
*/
string_view fileContent() const
string_view fileContent() const noexcept
{
return string_view{fileData(), fileLength()};
}
@ -92,6 +92,8 @@ class DROGON_EXPORT HttpFile
/// Return the file length.
size_t fileLength() const noexcept;
/// Return the content-type of the file.
drogon::ContentType getContentType() const noexcept;
/**
* @brief return the pointer of the file data.
*
@ -105,7 +107,10 @@ class DROGON_EXPORT HttpFile
const char *fileData() const noexcept;
/// Return the md5 string of the file
std::string getMd5() const;
std::string getMd5() const noexcept;
/// Return the content transfer encoding of the file.
const std::string &getContentTransferEncoding() const noexcept;
private:
std::shared_ptr<HttpFileImpl> implPtr_;

View File

@ -22,12 +22,12 @@
using namespace drogon;
int HttpFileImpl::save() const
int HttpFileImpl::save() const noexcept
{
return save(HttpAppFrameworkImpl::instance().getUploadPath());
}
int HttpFileImpl::save(const std::string &path) const
int HttpFileImpl::save(const std::string &path) const noexcept
{
assert(!path.empty());
if (fileName_.empty())
@ -79,7 +79,7 @@ int HttpFileImpl::save(const std::string &path) const
return saveTo(fsSaveToPath);
}
int HttpFileImpl::saveAs(const std::string &fileName) const
int HttpFileImpl::saveAs(const std::string &fileName) const noexcept
{
assert(!fileName.empty());
filesystem::path fsFileName(utils::toNativePath(fileName));
@ -105,7 +105,7 @@ int HttpFileImpl::saveAs(const std::string &fileName) const
}
return saveTo(fsFileName);
}
int HttpFileImpl::saveTo(const filesystem::path &pathAndFileName) const
int HttpFileImpl::saveTo(const filesystem::path &pathAndFileName) const noexcept
{
LOG_TRACE << "save uploaded file:" << pathAndFileName;
auto wPath = utils::toNativePath(pathAndFileName.native());
@ -123,47 +123,47 @@ int HttpFileImpl::saveTo(const filesystem::path &pathAndFileName) const
}
}
std::string HttpFileImpl::getMd5() const
std::string HttpFileImpl::getMd5() const noexcept
{
return utils::getMd5(fileContent_.data(), fileContent_.size());
}
const std::string &HttpFile::getFileName() const
const std::string &HttpFile::getFileName() const noexcept
{
return implPtr_->getFileName();
}
void HttpFile::setFileName(const std::string &fileName)
void HttpFile::setFileName(const std::string &fileName) noexcept
{
implPtr_->setFileName(fileName);
}
string_view HttpFile::getFileExtension() const
string_view HttpFile::getFileExtension() const noexcept
{
return implPtr_->getFileExtension();
}
FileType HttpFile::getFileType() const
FileType HttpFile::getFileType() const noexcept
{
return implPtr_->getFileType();
}
void HttpFile::setFile(const char *data, size_t length)
void HttpFile::setFile(const char *data, size_t length) noexcept
{
implPtr_->setFile(data, length);
}
int HttpFile::save() const
int HttpFile::save() const noexcept
{
return implPtr_->save();
}
int HttpFile::save(const std::string &path) const
int HttpFile::save(const std::string &path) const noexcept
{
return implPtr_->save(path);
}
int HttpFile::saveAs(const std::string &fileName) const
int HttpFile::saveAs(const std::string &fileName) const noexcept
{
return implPtr_->saveAs(fileName);
}
@ -173,22 +173,30 @@ size_t HttpFile::fileLength() const noexcept
return implPtr_->fileLength();
}
drogon::ContentType HttpFile::getContentType() const noexcept
{
return implPtr_->getContentType();
}
const char *HttpFile::fileData() const noexcept
{
return implPtr_->fileData();
}
std::string HttpFile::getMd5() const
std::string HttpFile::getMd5() const noexcept
{
return implPtr_->getMd5();
}
HttpFile::HttpFile(std::shared_ptr<HttpFileImpl> &&implPtr)
const std::string &HttpFile::getContentTransferEncoding() const noexcept
{
return implPtr_->getContentTransferEncoding();
}
HttpFile::HttpFile(std::shared_ptr<HttpFileImpl> &&implPtr) noexcept
: implPtr_(std::move(implPtr))
{
}
const std::string &HttpFile::getItemName() const
const std::string &HttpFile::getItemName() const noexcept
{
return implPtr_->getItemName();
}

View File

@ -29,26 +29,29 @@ class HttpFileImpl
{
public:
/// Return the file name;
const std::string &getFileName() const
const std::string &getFileName() const noexcept
{
return fileName_;
}
/// Set the file name, usually called by the MultiPartParser parser.
void setFileName(const std::string &fileName)
void setFileName(const std::string &fileName) noexcept
{
fileName_ = fileName;
}
void setFileName(std::string &&fileName) noexcept
{
fileName_ = std::move(fileName);
}
/// Return the file extension;
string_view getFileExtension() const
string_view getFileExtension() const noexcept
{
return drogon::getFileExtension(fileName_);
}
/// Set the contents of the file, usually called by the MultiPartParser
/// parser.
void setFile(const char *data, size_t length)
void setFile(const char *data, size_t length) noexcept
{
fileContent_ = string_view{data, length};
}
@ -58,7 +61,7 @@ class HttpFileImpl
* The folder saving the file is app().getUploadPath().
* The full path is app().getUploadPath()+"/"+this->getFileName()
*/
int save() const;
int save() const noexcept;
/// Save the file to @param path
/**
@ -67,7 +70,7 @@ class HttpFileImpl
* otherwise the file is saved as
* app().getUploadPath()+"/"+path+"/"+this->getFileName()
*/
int save(const std::string &path) const;
int save(const std::string &path) const noexcept;
/// Save the file to file system with a new name
/**
@ -75,7 +78,7 @@ class HttpFileImpl
* the full path is app().getUploadPath()+"/"+filename, otherwise the file
* is saved as the filename
*/
int saveAs(const std::string &fileName) const;
int saveAs(const std::string &fileName) const noexcept;
/// Return the file length.
size_t fileLength() const noexcept
@ -94,35 +97,62 @@ class HttpFileImpl
}
/// Return the name of the item in multiple parts.
const std::string &getItemName() const
const std::string &getItemName() const noexcept
{
return itemName_;
}
void setItemName(const std::string &itemName)
void setItemName(const std::string &itemName) noexcept
{
itemName_ = itemName;
}
void setItemName(std::string &&itemName) noexcept
{
itemName_ = std::move(itemName);
}
/// Return the type of file.
FileType getFileType() const
FileType getFileType() const noexcept
{
return parseFileType(getFileExtension());
}
/// Return the md5 string of the file
std::string getMd5() const;
std::string getMd5() const noexcept;
// int saveTo(const std::string &pathAndFileName) const;
int saveTo(const filesystem::path &pathAndFileName) const;
void setRequest(const HttpRequestPtr &req)
int saveTo(const filesystem::path &pathAndFileName) const noexcept;
void setRequest(const HttpRequestPtr &req) noexcept
{
requestPtr_ = req;
}
drogon::ContentType getContentType() const noexcept
{
return contentType_;
}
void setContentType(drogon::ContentType contentType) noexcept
{
contentType_ = contentType;
}
void setContentTransferEncoding(
const std::string &contentTransferEncoding) noexcept
{
transferEncoding_ = contentTransferEncoding;
}
void setContentTransferEncoding(
std::string &&contentTransferEncoding) noexcept
{
transferEncoding_ = std::move(contentTransferEncoding);
}
const std::string &getContentTransferEncoding() const noexcept
{
return transferEncoding_;
}
private:
std::string fileName_;
std::string itemName_;
std::string transferEncoding_;
string_view fileContent_;
HttpRequestPtr requestPtr_;
drogon::ContentType contentType_{drogon::CT_NONE};
};
} // namespace drogon

View File

@ -75,69 +75,132 @@ int MultiPartParser::parse(const HttpRequestPtr &req)
contentType.data() + (pos + 9),
contentType.size() - (pos + 9));
}
static std::pair<string_view, string_view> parseLine(const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(string_view(begin, p - begin),
string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(string_view(begin, p - begin),
string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(string_view(), string_view());
}
int MultiPartParser::parseEntity(const char *begin, const char *end)
{
static const char entityName[] = "name=";
static const char semiColon[] = ";";
static const char fileName[] = "filename=";
static const char CRLF[] = "\r\n\r\n";
auto pos = std::search(begin, end, entityName, entityName + 5);
if (pos == end)
auto headEnd = std::search(begin, end, CRLF, CRLF + 4);
if (headEnd == end)
{
return -1;
pos += 5;
auto pos1 = std::search(pos, end, semiColon, semiColon + 1);
if (pos1 == end)
{
pos1 = std::search(pos, end, CRLF, CRLF + 2);
if (pos1 == end)
return -1;
}
if (*pos == '"')
pos++;
if (*(pos1 - 1) == '"')
pos1--;
std::string name(pos, pos1);
pos = std::search(pos1, end, fileName, fileName + 9);
if (pos == end)
headEnd += 2;
auto pos = begin;
std::shared_ptr<HttpFileImpl> filePtr = std::make_shared<HttpFileImpl>();
while (pos != headEnd)
{
pos1 = std::search(pos1, end, CRLF, CRLF + 4);
if (pos1 == end)
auto lineEnd = std::search(pos, headEnd, CRLF, CRLF + 2);
auto keyAndValue = parseLine(pos, lineEnd);
if (keyAndValue.first.empty() || keyAndValue.second.empty())
{
return -1;
parameters_[name] = std::string(pos1 + 4, end);
}
pos = lineEnd + 2;
std::string key{keyAndValue.first.data(), keyAndValue.first.size()};
std::transform(key.begin(),
key.end(),
key.begin(),
[](unsigned char c) { return tolower(c); });
if (key == "content-disposition")
{
auto value = keyAndValue.second;
auto valueEnd = value.data() + value.length();
auto namePos =
std::search(value.data(), valueEnd, entityName, entityName + 5);
if (namePos == valueEnd)
{
return -1;
}
namePos += 5;
const char *nameEnd;
if (*namePos == '"')
{
++namePos;
nameEnd = std::find(namePos, valueEnd, '"');
}
else
{
nameEnd = std::find(namePos, valueEnd, ';');
}
std::string name(namePos, nameEnd);
auto fileNamePos =
std::search(nameEnd, valueEnd, fileName, fileName + 9);
if (fileNamePos == valueEnd)
{
parameters_.emplace(name, std::string(headEnd + 2, end));
return 0;
}
else
{
fileNamePos += 9;
const char *fileNameEnd;
if (*fileNamePos == '"')
{
++fileNamePos;
fileNameEnd = std::find(fileNamePos, valueEnd, '"');
}
else
{
fileNameEnd = std::find(fileNamePos, valueEnd, ';');
}
std::string fName{fileNamePos, fileNameEnd};
filePtr->setRequest(requestPtr_);
filePtr->setItemName(std::move(name));
filePtr->setFileName(std::move(fName));
filePtr->setFile(headEnd + 2,
static_cast<size_t>(end - headEnd - 2));
}
}
else if (key == "content-type")
{
auto value = keyAndValue.second;
auto semiColonPos =
std::find(value.data(), value.data() + value.length(), ';');
string_view contentType(value.data(), semiColonPos - value.data());
filePtr->setContentType(parseContentType(contentType));
}
else if (key == "content-transfer-encoding")
{
auto value = keyAndValue.second;
auto semiColonPos =
std::find(value.data(), value.data() + value.length(), ';');
filePtr->setContentTransferEncoding(
std::string{value.data(), semiColonPos});
}
}
if (!filePtr->getFileName().empty())
{
files_.emplace_back(std::move(filePtr));
return 0;
}
else
{
pos += 9;
pos1 = std::search(pos, end, semiColon, semiColon + 1);
if (pos1 == end)
{
pos1 = std::search(pos, end, CRLF, CRLF + 2);
if (pos1 == end)
return -1;
}
else
{
auto pos2 = std::search(pos, pos1, CRLF, CRLF + 2);
if (pos2 != end)
pos1 = pos2;
}
if (*pos == '"')
pos++;
if (*(pos1 - 1) == '"')
pos1--;
auto filePtr = std::make_shared<HttpFileImpl>();
filePtr->setRequest(requestPtr_);
filePtr->setItemName(name);
filePtr->setFileName(std::string(pos, pos1));
pos1 = std::search(pos1, end, CRLF, CRLF + 4);
if (pos1 == end)
return -1;
filePtr->setFile(pos1 + 4, static_cast<size_t>(end - pos1 - 4));
files_.emplace_back(std::move(filePtr));
return 0;
return -1;
}
}
@ -153,7 +216,7 @@ int MultiPartParser::parse(const HttpRequestPtr &req,
pos1 = 0;
auto content = static_cast<HttpRequestImpl *>(req.get())->bodyView();
pos2 = content.find(boundary);
while (1)
while (true)
{
pos1 = pos2;
if (pos1 == string_view::npos)
@ -164,13 +227,17 @@ int MultiPartParser::parse(const HttpRequestPtr &req,
pos2 = content.find(boundary, pos1);
if (pos2 == string_view::npos)
break;
// std::cout<<"pos1="<<pos1<<" pos2="<<pos2<<std::endl;
bool flag = false;
if (content[pos2 - 4] == '\r' && content[pos2 - 3] == '\n' &&
content[pos2 - 2] == '-' && content[pos2 - 1] == '-')
{
pos2 -= 4;
flag = true;
}
if (parseEntity(content.data() + pos1, content.data() + pos2) != 0)
return -1;
// pos2+=boundary.length();
if (flag)
pos2 += 4;
}
return 0;
}

View File

@ -21,7 +21,8 @@ set(UNITTEST_SOURCES
unittests/MainLoopTest.cc
unittests/CacheMapTest.cc
unittests/StringOpsTest.cc
unittests/ControllerCreationTest.cc)
unittests/ControllerCreationTest.cc
unittests/MultiPartParserTest.cc)
if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} unittests/CoroutineTest.cc)

View File

@ -0,0 +1,62 @@
#include <drogon/MultiPart.h>
#include <drogon/drogon_test.h>
#include <drogon/HttpRequest.h>
DROGON_TEST(MultiPartParser)
{
drogon::MultiPartParser parser1;
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Post);
req->addHeader("content-type", "multipart/form-data; boundary=\"12345\"");
req->setBody(
"--12345\r\n"
"Content-Disposition: form-data; name=\"somekey\"\r\n"
"\r\n"
"Hello; World\r\n"
"--12345--");
CHECK(0 == parser1.parse(req));
CHECK(parser1.getParameters().size() == 1);
CHECK(parser1.getParameters().at("somekey") == "Hello; World");
req->setBody(
"--12345\r\n"
"Content-Disposition: form-data; name=\"somekey\"; "
"filename=\"test\"\r\n"
"\r\n"
"Hello; World\r\n"
"--12345--");
drogon::MultiPartParser parser2;
CHECK(0 == parser2.parse(req));
auto filesMap = parser2.getFilesMap();
CHECK(filesMap.size() == 1);
CHECK(filesMap.at("somekey").getFileName() == "test");
CHECK(filesMap.at("somekey").fileContent() == "Hello; World");
req->setBody(
"--12345\r\n"
"Content-Disposition: form-data; name=\"name of pdf\"; "
"filename=\"pdf-file.pdf\"\r\n"
"Content-Type: application/octet-stream\r\n"
"content-transfer-encoding: quoted-printable\r\n"
"\r\n"
"bytes of pdf file\r\n"
"--12345--");
drogon::MultiPartParser parser3;
CHECK(0 == parser3.parse(req));
filesMap = parser3.getFilesMap();
CHECK(filesMap.size() == 1);
CHECK(filesMap.at("name of pdf").getFileName() == "pdf-file.pdf");
CHECK(filesMap.at("name of pdf").fileContent() == "bytes of pdf file");
CHECK(filesMap.at("name of pdf").getContentType() ==
drogon::CT_APPLICATION_OCTET_STREAM);
CHECK(filesMap.at("name of pdf").getContentTransferEncoding() ==
"quoted-printable");
req->setBody(
"--12345\r\n"
"Content-Disposition: form-data; name=\"some;key\"\r\n"
"\r\n"
"Hello; World\r\n"
"--12345--");
drogon::MultiPartParser parser4;
CHECK(0 == parser4.parse(req));
CHECK(parser4.getParameters().size() == 1);
CHECK(parser4.getParameters().at("some;key") == "Hello; World");
}