mirror of
https://github.com/yhirose/cpp-httplib
synced 2024-11-21 14:29:10 -07:00
Fix #1519
This commit is contained in:
parent
afb0674ccb
commit
aabf752a51
2 changed files with 127 additions and 12 deletions
76
httplib.h
76
httplib.h
|
@ -2428,6 +2428,13 @@ inline std::string trim_copy(const std::string &s) {
|
||||||
return s.substr(r.first, r.second - r.first);
|
return s.substr(r.first, r.second - r.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string trim_double_quotes_copy(const std::string &s) {
|
||||||
|
if (s.length() >= 2 && s.front() == '"' && s.back() == '"') {
|
||||||
|
return s.substr(1, s.size() - 2);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
inline void split(const char *b, const char *e, char d,
|
inline void split(const char *b, const char *e, char d,
|
||||||
std::function<void(const char *, const char *)> fn) {
|
std::function<void(const char *, const char *)> fn) {
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
|
@ -4064,14 +4071,34 @@ inline bool parse_multipart_boundary(const std::string &content_type,
|
||||||
if (pos == std::string::npos) { return false; }
|
if (pos == std::string::npos) { return false; }
|
||||||
auto end = content_type.find(';', pos);
|
auto end = content_type.find(';', pos);
|
||||||
auto beg = pos + strlen(boundary_keyword);
|
auto beg = pos + strlen(boundary_keyword);
|
||||||
boundary = content_type.substr(beg, end - beg);
|
boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg));
|
||||||
if (boundary.length() >= 2 && boundary.front() == '"' &&
|
|
||||||
boundary.back() == '"') {
|
|
||||||
boundary = boundary.substr(1, boundary.size() - 2);
|
|
||||||
}
|
|
||||||
return !boundary.empty();
|
return !boundary.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void parse_disposition_params(const std::string &s, Params ¶ms) {
|
||||||
|
std::set<std::string> cache;
|
||||||
|
split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {
|
||||||
|
std::string kv(b, e);
|
||||||
|
if (cache.find(kv) != cache.end()) { return; }
|
||||||
|
cache.insert(kv);
|
||||||
|
|
||||||
|
std::string key;
|
||||||
|
std::string val;
|
||||||
|
split(b, e, '=', [&](const char *b2, const char *e2) {
|
||||||
|
if (key.empty()) {
|
||||||
|
key.assign(b2, e2);
|
||||||
|
} else {
|
||||||
|
val.assign(b2, e2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!key.empty()) {
|
||||||
|
params.emplace(trim_double_quotes_copy((key)),
|
||||||
|
trim_double_quotes_copy((val)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
inline bool parse_range_header(const std::string &s, Ranges &ranges) {
|
inline bool parse_range_header(const std::string &s, Ranges &ranges) {
|
||||||
#else
|
#else
|
||||||
|
@ -4129,11 +4156,6 @@ public:
|
||||||
bool parse(const char *buf, size_t n, const ContentReceiver &content_callback,
|
bool parse(const char *buf, size_t n, const ContentReceiver &content_callback,
|
||||||
const MultipartContentHeader &header_callback) {
|
const MultipartContentHeader &header_callback) {
|
||||||
|
|
||||||
// TODO: support 'filename*'
|
|
||||||
static const std::regex re_content_disposition(
|
|
||||||
R"~(^Content-Disposition:\s*form-data;\s*name="(.*?)"(?:;\s*filename="(.*?)")?(?:;\s*filename\*=\S+)?\s*$)~",
|
|
||||||
std::regex_constants::icase);
|
|
||||||
|
|
||||||
buf_append(buf, n);
|
buf_append(buf, n);
|
||||||
|
|
||||||
while (buf_size() > 0) {
|
while (buf_size() > 0) {
|
||||||
|
@ -4171,10 +4193,40 @@ public:
|
||||||
if (start_with_case_ignore(header, header_name)) {
|
if (start_with_case_ignore(header, header_name)) {
|
||||||
file_.content_type = trim_copy(header.substr(header_name.size()));
|
file_.content_type = trim_copy(header.substr(header_name.size()));
|
||||||
} else {
|
} else {
|
||||||
|
static const std::regex re_content_disposition(
|
||||||
|
R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~",
|
||||||
|
std::regex_constants::icase);
|
||||||
|
|
||||||
std::smatch m;
|
std::smatch m;
|
||||||
if (std::regex_match(header, m, re_content_disposition)) {
|
if (std::regex_match(header, m, re_content_disposition)) {
|
||||||
file_.name = m[1];
|
Params params;
|
||||||
file_.filename = m[2];
|
parse_disposition_params(m[1], params);
|
||||||
|
|
||||||
|
auto it = params.find("name");
|
||||||
|
if (it != params.end()) {
|
||||||
|
file_.name = it->second;
|
||||||
|
} else {
|
||||||
|
is_valid_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
it = params.find("filename");
|
||||||
|
if (it != params.end()) { file_.filename = it->second; }
|
||||||
|
|
||||||
|
it = params.find("filename*");
|
||||||
|
if (it != params.end()) {
|
||||||
|
// Only allow UTF-8 enconnding...
|
||||||
|
static const std::regex re_rfc5987_encoding(
|
||||||
|
R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase);
|
||||||
|
|
||||||
|
std::smatch m2;
|
||||||
|
if (std::regex_match(it->second, m2, re_rfc5987_encoding)) {
|
||||||
|
file_.filename = decode_url(m2[1], false); // override...
|
||||||
|
} else {
|
||||||
|
is_valid_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
is_valid_ = false;
|
is_valid_ = false;
|
||||||
return false;
|
return false;
|
||||||
|
|
63
test/test.cc
63
test/test.cc
|
@ -6105,6 +6105,69 @@ TEST(MultipartFormDataTest, PutInvalidBoundaryChars) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MultipartFormDataTest, AlternateFilename) {
|
||||||
|
Server svr;
|
||||||
|
svr.Post("/test", [&](const Request &req, Response &res) {
|
||||||
|
ASSERT_EQ(3u, req.files.size());
|
||||||
|
|
||||||
|
auto it = req.files.begin();
|
||||||
|
ASSERT_EQ("file1", it->second.name);
|
||||||
|
ASSERT_EQ("A.txt", it->second.filename);
|
||||||
|
ASSERT_EQ("text/plain", it->second.content_type);
|
||||||
|
ASSERT_EQ("Content of a.txt.\r\n", it->second.content);
|
||||||
|
|
||||||
|
++it;
|
||||||
|
ASSERT_EQ("file2", it->second.name);
|
||||||
|
ASSERT_EQ("a.html", it->second.filename);
|
||||||
|
ASSERT_EQ("text/html", it->second.content_type);
|
||||||
|
ASSERT_EQ("<!DOCTYPE html><title>Content of a.html.</title>\r\n",
|
||||||
|
it->second.content);
|
||||||
|
|
||||||
|
++it;
|
||||||
|
ASSERT_EQ("text", it->second.name);
|
||||||
|
ASSERT_EQ("", it->second.filename);
|
||||||
|
ASSERT_EQ("", it->second.content_type);
|
||||||
|
ASSERT_EQ("text default", it->second.content);
|
||||||
|
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
auto req = "POST /test HTTP/1.1\r\n"
|
||||||
|
"Content-Type: multipart/form-data;boundary=--------\r\n"
|
||||||
|
"Content-Length: 399\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"----------\r\n"
|
||||||
|
"Content-Disposition: form-data; name=\"text\"\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"text default\r\n"
|
||||||
|
"----------\r\n"
|
||||||
|
"Content-Disposition: form-data; filename*=\"UTF-8''\%41.txt\"; "
|
||||||
|
"filename=\"a.txt\"; name=\"file1\"\r\n"
|
||||||
|
"Content-Type: text/plain\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"Content of a.txt.\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"----------\r\n"
|
||||||
|
"Content-Disposition: form-data; name=\"file2\" ;filename = "
|
||||||
|
"\"a.html\"\r\n"
|
||||||
|
"Content-Type: text/html\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"<!DOCTYPE html><title>Content of a.html.</title>\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"------------\r\n";
|
||||||
|
|
||||||
|
ASSERT_TRUE(send_request(1, req));
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
|
|
Loading…
Reference in a new issue