mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6a2365ca5 | |||
| df1ff7510b | |||
| 379905bd34 | |||
| 66719ae3d4 | |||
| bc9251ea49 | |||
| a9e942d755 | |||
| e1785d6723 | |||
| b9539b8921 | |||
| 4c93b973ff | |||
| 033bc35723 |
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user