Compare commits

...

10 Commits

Author SHA1 Message Date
yhirose f6a2365ca5 Fix #282 2019-12-06 12:21:15 -05:00
yhirose df1ff7510b Made code more readable 2019-12-06 12:02:08 -05:00
yhirose 379905bd34 Merge branch 'whitespace-and-libcxx-compat' of https://github.com/matvore/cpp-httplib 2019-12-06 09:51:21 -05:00
yhirose 66719ae3d4 Merge pull request #283 from barryam3/noexcept
Remove use of exceptions.
2019-12-05 21:32:06 -05:00
Matthew DeVore bc9251ea49 Work around incompatibility in <regex> in libc++
libc++ (the implementation of the C++ standard library usually used by
Clang) throws an exception for the regex used by parse_headers before
this patch for certain strings. Work around this by simplifying the
regex and parsing the header lines "by hand" partially. I have repro'd
this problem with Xcode 11.1 which I believe uses libc++ version 8.

This may be a bug in libc++ as I can't see why the regex would result in
asymptotic run-time complexity for any strings. However, it may take a
while for libc++ to be fixed and for everyone to migrate to it, so it
makes sense to work around it in this codebase for now.
2019-12-05 17:14:16 -08:00
Matthew DeVore a9e942d755 Properly trim whitespace from headers
HTTP Whitespace and regex whitespace are not the same, so we can't use
\s in regexes when parsing HTTP headers. Instead, explicitly specify
what is considered whitespace in the regex.
2019-12-05 17:14:16 -08:00
Barry McNamara e1785d6723 Remove use of exceptions. 2019-12-05 15:56:55 -08:00
yhirose b9539b8921 Fixed build errors 2019-12-03 10:30:07 -05:00
yhirose 4c93b973ff Fixed typo in README 2019-12-02 09:50:52 -05:00
yhirose 033bc35723 Improve multipart content reader interface 2019-12-02 07:11:12 -05:00
5 changed files with 200 additions and 92 deletions
+5 -5
View File
@@ -121,14 +121,14 @@ svr.Post("/content_receiver",
if (req.is_multipart_form_data()) {
MultipartFiles files;
content_reader(
[&](const std::string &name, const MultipartFile &file) {
files.emplace(name, file);
return true;
},
[&](const std::string &name, const char *data, size_t data_length) {
auto &file = files.find(name)->second;
file.content.append(data, data_length);
return true;
},
[&](const std::string &name, const MultipartFile &file) {
files.emplace(name, file);
return true;
});
} else {
std::string body;
@@ -156,7 +156,7 @@ svr.Get("/chunked", [&](const Request& req, Response& res) {
});
```
### Default thread pool supporet
### Default thread pool support
Set thread count to 8:
+1 -4
View File
@@ -46,10 +46,7 @@ string dump_multipart_files(const MultipartFiles &files) {
snprintf(buf, sizeof(buf), "content type: %s\n", file.content_type.c_str());
s += buf;
snprintf(buf, sizeof(buf), "text offset: %lu\n", file.offset);
s += buf;
snprintf(buf, sizeof(buf), "text length: %lu\n", file.length);
snprintf(buf, sizeof(buf), "text length: %lu\n", file.content.size());
s += buf;
s += "----------------\n";
+2 -2
View File
@@ -37,10 +37,10 @@ int main(void) {
svr.Post("/post", [](const Request & req, Response &res) {
auto file = req.get_file_value("file");
cout << "file: " << file.offset << ":" << file.length << ":" << file.filename << endl;
cout << "file length: " << file.content.length() << ":" << file.filename << endl;
ofstream ofs(file.filename, ios::binary);
ofs << req.body.substr(file.offset, file.length);
ofs << file.content;
res.set_content("done", "text/plain");
});
+105 -77
View File
@@ -225,22 +225,22 @@ using MultipartFormDataItems = std::vector<MultipartFormData>;
using ContentReceiver =
std::function<bool(const char *data, size_t data_length)>;
using MultipartContentReceiver =
std::function<bool(const std::string& name, const char *data, size_t data_length)>;
using MultipartContentHeader =
std::function<bool(const std::string &name, const MultipartFile &file)>;
using MultipartContentReceiver =
std::function<bool(const std::string& name, const char *data, size_t data_length)>;
class ContentReader {
public:
using Reader = std::function<bool(ContentReceiver receiver)>;
using MultipartReader = std::function<bool(MultipartContentReceiver receiver, MultipartContentHeader header)>;
using MultipartReader = std::function<bool(MultipartContentHeader header, MultipartContentReceiver receiver)>;
ContentReader(Reader reader, MultipartReader muitlpart_reader)
: reader_(reader), muitlpart_reader_(muitlpart_reader) {}
bool operator()(MultipartContentReceiver receiver, MultipartContentHeader header) const {
return muitlpart_reader_(receiver, header);
bool operator()(MultipartContentHeader header, MultipartContentReceiver receiver) const {
return muitlpart_reader_(header, receiver);
}
bool operator()(ContentReceiver receiver) const {
@@ -591,13 +591,13 @@ private:
bool read_content_with_content_receiver(Stream &strm, bool last_connection,
Request &req, Response &res,
ContentReceiver receiver,
MultipartContentReceiver multipart_receiver,
MultipartContentHeader multipart_header);
MultipartContentHeader multipart_header,
MultipartContentReceiver multipart_receiver);
bool read_content_core(Stream &strm, bool last_connection,
Request &req, Response &res,
ContentReceiver receiver,
MultipartContentReceiver multipart_receiver,
MultipartContentHeader mulitpart_header);
MultipartContentHeader mulitpart_header,
MultipartContentReceiver multipart_receiver);
virtual bool process_and_close_socket(socket_t sock);
@@ -1114,6 +1114,11 @@ public:
}
}
bool end_with_crlf() const {
auto end = ptr() + size();
return size() >= 2 && end[-2] == '\r' && end[-1] == '\n';
}
bool getline() {
fixed_buffer_used_size_ = 0;
glowable_buffer_.clear();
@@ -1357,6 +1362,26 @@ inline bool is_connection_error() {
#endif
}
inline socket_t create_client_socket(
const char *host, int port, time_t timeout_sec) {
return create_socket(
host, port, [=](socket_t sock, struct addrinfo &ai) -> bool {
set_nonblocking(sock, true);
auto ret = ::connect(sock, ai.ai_addr, static_cast<int>(ai.ai_addrlen));
if (ret < 0) {
if (is_connection_error() ||
!wait_until_socket_is_ready(sock, timeout_sec, 0)) {
close_socket(sock);
return false;
}
}
set_nonblocking(sock, false);
return true;
});
}
inline std::string get_remote_addr(socket_t sock) {
struct sockaddr_storage addr;
socklen_t len = sizeof(addr);
@@ -1542,18 +1567,35 @@ inline uint64_t get_header_value_uint64(const Headers &headers, const char *key,
}
inline bool read_headers(Stream &strm, Headers &headers) {
static std::regex re(R"((.+?):\s*(.+?)\s*\r\n)");
const auto bufsiz = 2048;
char buf[bufsiz];
stream_line_reader line_reader(strm, buf, bufsiz);
for (;;) {
if (!line_reader.getline()) { return false; }
if (!strcmp(line_reader.ptr(), "\r\n")) { break; }
// Check if the line ends with CRLF.
if (line_reader.end_with_crlf()) {
// Blank line indicates end of headers.
if (line_reader.size() == 2) { break; }
} else {
continue; // Skip invalid line.
}
// Skip trailing spaces and tabs.
auto end = line_reader.ptr() + line_reader.size() - 2;
while (line_reader.ptr() < end && (end[-1] == ' ' || end[-1] == '\t')) {
end--;
}
// Horizontal tab and ' ' are considered whitespace and are ignored when on
// the left or right side of the header value:
// - https://stackoverflow.com/questions/50179659/
// - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
static const std::regex re(R"((.+?):[\t ]*(.+))");
std::cmatch m;
if (std::regex_match(line_reader.ptr(), m, re)) {
if (std::regex_match(line_reader.ptr(), end, m, re)) {
auto key = std::string(m[1]);
auto val = std::string(m[2]);
headers.emplace(key, val);
@@ -1881,38 +1923,39 @@ inline bool parse_multipart_boundary(const std::string &content_type,
}
inline bool parse_range_header(const std::string &s, Ranges &ranges) {
try {
static auto re_first_range =
std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))");
std::smatch m;
if (std::regex_match(s, m, re_first_range)) {
auto pos = m.position(1);
auto len = m.length(1);
detail::split(
&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {
static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))");
std::cmatch m;
if (std::regex_match(b, e, m, re_another_range)) {
ssize_t first = -1;
if (!m.str(1).empty()) {
first = static_cast<ssize_t>(std::stoll(m.str(1)));
}
ssize_t last = -1;
if (!m.str(2).empty()) {
last = static_cast<ssize_t>(std::stoll(m.str(2)));
}
if (first != -1 && last != -1 && first > last) {
throw std::runtime_error("invalid range error");
}
ranges.emplace_back(std::make_pair(first, last));
static auto re_first_range =
std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))");
std::smatch m;
if (std::regex_match(s, m, re_first_range)) {
auto pos = m.position(1);
auto len = m.length(1);
bool all_valid_ranges = true;
detail::split(
&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) {
if (!all_valid_ranges) return;
static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))");
std::cmatch m;
if (std::regex_match(b, e, m, re_another_range)) {
ssize_t first = -1;
if (!m.str(1).empty()) {
first = static_cast<ssize_t>(std::stoll(m.str(1)));
}
});
return true;
}
return false;
} catch (...) { return false; }
ssize_t last = -1;
if (!m.str(2).empty()) {
last = static_cast<ssize_t>(std::stoll(m.str(2)));
}
if (first != -1 && last != -1 && first > last) {
all_valid_ranges = false;
return;
}
ranges.emplace_back(std::make_pair(first, last));
}
});
return all_valid_ranges;
}
return false;
}
class MultipartFormDataParser {
@@ -2035,17 +2078,15 @@ public:
break;
}
case 4: { // Boundary
auto pos = buf_.find(crlf_);
if (crlf_.size() > buf_.size()) { return true; }
if (pos == 0) {
if (buf_.find(crlf_) == 0) {
buf_.erase(0, crlf_.size());
off_ += crlf_.size();
state_ = 1;
} else {
auto pattern = dash_ + crlf_;
if (pattern.size() > buf_.size()) { return true; }
auto pos = buf_.find(pattern);
if (pos == 0) {
if (buf_.find(pattern) == 0) {
buf_.erase(0, pattern.size());
off_ += pattern.size();
is_valid_ = true;
@@ -2796,11 +2837,17 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
inline bool Server::read_content(Stream &strm, bool last_connection,
Request &req, Response &res) {
auto ret = read_content_core(strm, last_connection, req, res,
// Regular
[&](const char *buf, size_t n) {
if (req.body.size() + n > req.body.max_size()) { return false; }
req.body.append(buf, n);
return true;
},
// Multipart
[&](const std::string &name, const MultipartFile &file) {
req.files.emplace(name, file);
return true;
},
[&](const std::string &name, const char *buf, size_t n) {
// TODO: handle elements with a same key
auto it = req.files.find(name);
@@ -2808,10 +2855,6 @@ inline bool Server::read_content(Stream &strm, bool last_connection,
if (content.size() + n > content.max_size()) { return false; }
content.append(buf, n);
return true;
},
[&](const std::string &name, const MultipartFile &file) {
req.files.emplace(name, file);
return true;
}
);
@@ -2827,18 +2870,18 @@ inline bool
Server::read_content_with_content_receiver(Stream &strm, bool last_connection,
Request &req, Response &res,
ContentReceiver receiver,
MultipartContentReceiver multipart_receiver,
MultipartContentHeader multipart_header) {
MultipartContentHeader multipart_header,
MultipartContentReceiver multipart_receiver) {
return read_content_core(strm, last_connection, req, res,
receiver, multipart_receiver, multipart_header);
receiver, multipart_header, multipart_receiver);
}
inline bool
Server::read_content_core(Stream &strm, bool last_connection,
Request &req, Response &res,
ContentReceiver receiver,
MultipartContentReceiver multipart_receiver,
MultipartContentHeader mulitpart_header) {
MultipartContentHeader mulitpart_header,
MultipartContentReceiver multipart_receiver) {
detail::MultipartFormDataParser multipart_form_data_parser;
ContentReceiver out;
@@ -3001,9 +3044,9 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm,
return read_content_with_content_receiver(strm, last_connection, req, res,
receiver, nullptr, nullptr);
},
[&](MultipartContentReceiver receiver, MultipartContentHeader header) {
[&](MultipartContentHeader header, MultipartContentReceiver receiver) {
return read_content_with_content_receiver(strm, last_connection, req, res,
nullptr, receiver, header);
nullptr, header, receiver);
}
);
@@ -3164,22 +3207,7 @@ inline Client::~Client() {}
inline bool Client::is_valid() const { return true; }
inline socket_t Client::create_client_socket() const {
return detail::create_socket(
host_.c_str(), port_, [=](socket_t sock, struct addrinfo &ai) -> bool {
detail::set_nonblocking(sock, true);
auto ret = connect(sock, ai.ai_addr, static_cast<int>(ai.ai_addrlen));
if (ret < 0) {
if (detail::is_connection_error() ||
!detail::wait_until_socket_is_ready(sock, timeout_sec_, 0)) {
detail::close_socket(sock);
return false;
}
}
detail::set_nonblocking(sock, false);
return true;
});
return detail::create_client_socket(host_.c_str(), port_, timeout_sec_);
}
inline bool Client::read_response_line(Stream &strm, Response &res) {
+87 -4
View File
@@ -761,14 +761,14 @@ protected:
if (req.is_multipart_form_data()) {
MultipartFiles files;
content_reader(
[&](const std::string &name, const MultipartFile &file) {
files.emplace(name, file);
return true;
},
[&](const std::string &name, const char *data, size_t data_length) {
auto &file = files.find(name)->second;
file.content.append(data, data_length);
return true;
},
[&](const std::string &name, const MultipartFile &file) {
files.emplace(name, file);
return true;
});
EXPECT_EQ(5u, files.size());
@@ -1766,6 +1766,89 @@ TEST_F(ServerTest, MultipartFormDataGzip) {
}
#endif
// Sends a raw request to a server listening at HOST:PORT.
static bool send_request(time_t read_timeout_sec, const std::string& req) {
auto client_sock =
detail::create_client_socket(HOST, PORT, /*timeout_sec=*/5);
if (client_sock == INVALID_SOCKET) { return false; }
return detail::process_and_close_socket(
true, client_sock, 1, read_timeout_sec, 0,
[&](Stream& strm, bool /*last_connection*/,
bool &/*connection_close*/) -> bool {
if (req.size() !=
static_cast<size_t>(strm.write(req.data(), req.size()))) {
return false;
}
char buf[512];
detail::stream_line_reader line_reader(strm, buf, sizeof(buf));
while (line_reader.getline()) {}
return true;
});
}
TEST(ServerRequestParsingTest, TrimWhitespaceFromHeaderValues) {
Server svr;
std::string header_value;
svr.Get("/validate-ws-in-headers",
[&](const Request &req, Response &res) {
header_value = req.get_header_value("foo");
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
while (!svr.is_running()) {
msleep(1);
}
// Only space and horizontal tab are whitespace. Make sure other whitespace-
// like characters are not treated the same - use vertical tab and escape.
const std::string req =
"GET /validate-ws-in-headers HTTP/1.1\r\n"
"foo: \t \v bar \e\t \r\n"
"Connection: close\r\n"
"\r\n";
ASSERT_TRUE(send_request(5, req));
svr.stop();
t.join();
EXPECT_EQ(header_value, "\v bar \e");
}
TEST(ServerRequestParsingTest, ReadHeadersRegexComplexity) {
Server svr;
svr.Get("/hi",
[&](const Request & /*req*/, Response &res) {
res.set_content("ok", "text/plain");
});
// Server read timeout must be longer than the client read timeout for the
// bug to reproduce, probably to force the server to process a request
// without a trailing blank line.
const time_t client_read_timeout_sec = 1;
svr.set_read_timeout(client_read_timeout_sec + 1, 0);
bool listen_thread_ok = false;
thread t = thread([&] { listen_thread_ok = svr.listen(HOST, PORT); });
while (!svr.is_running()) {
msleep(1);
}
// A certain header line causes an exception if the header property is parsed
// naively with a single regex. This occurs with libc++ but not libstdc++.
const std::string req =
"GET /hi HTTP/1.1\r\n"
" : "
" ";
ASSERT_TRUE(send_request(client_read_timeout_sec, req));
svr.stop();
t.join();
EXPECT_TRUE(listen_thread_ok);
}
class ServerTestWithAI_PASSIVE : public ::testing::Test {
protected:
ServerTestWithAI_PASSIVE()