Skip to content

Commit 379905b

Browse files
committed
Merge branch 'whitespace-and-libcxx-compat' of https://github.com/matvore/cpp-httplib
2 parents 66719ae + bc9251e commit 379905b

File tree

2 files changed

+125
-19
lines changed

2 files changed

+125
-19
lines changed

httplib.h

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,26 @@ inline bool is_connection_error() {
13571357
#endif
13581358
}
13591359

1360+
inline socket_t create_client_socket(
1361+
const char *host, int port, time_t timeout_sec) {
1362+
return create_socket(
1363+
host, port, [=](socket_t sock, struct addrinfo &ai) -> bool {
1364+
set_nonblocking(sock, true);
1365+
1366+
auto ret = ::connect(sock, ai.ai_addr, static_cast<int>(ai.ai_addrlen));
1367+
if (ret < 0) {
1368+
if (is_connection_error() ||
1369+
!wait_until_socket_is_ready(sock, timeout_sec, 0)) {
1370+
close_socket(sock);
1371+
return false;
1372+
}
1373+
}
1374+
1375+
set_nonblocking(sock, false);
1376+
return true;
1377+
});
1378+
}
1379+
13601380
inline std::string get_remote_addr(socket_t sock) {
13611381
struct sockaddr_storage addr;
13621382
socklen_t len = sizeof(addr);
@@ -1542,7 +1562,11 @@ inline uint64_t get_header_value_uint64(const Headers &headers, const char *key,
15421562
}
15431563

15441564
inline bool read_headers(Stream &strm, Headers &headers) {
1545-
static std::regex re(R"((.+?):\s*(.+?)\s*\r\n)");
1565+
// Horizontal tab and ' ' are considered whitespace and are ignored when on
1566+
// the left or right side of the header value:
1567+
// - https://stackoverflow.com/questions/50179659/
1568+
// - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
1569+
static std::regex re(R"((.+?):[\t ]*(.+))");
15461570

15471571
const auto bufsiz = 2048;
15481572
char buf[bufsiz];
@@ -1551,9 +1575,23 @@ inline bool read_headers(Stream &strm, Headers &headers) {
15511575

15521576
for (;;) {
15531577
if (!line_reader.getline()) { return false; }
1554-
if (!strcmp(line_reader.ptr(), "\r\n")) { break; }
1578+
const char *end = line_reader.ptr() + line_reader.size();
1579+
auto erase_last_char = [&](char c) {
1580+
if (line_reader.ptr() == end || end[-1] != c) {
1581+
return false;
1582+
}
1583+
end--;
1584+
return true;
1585+
};
1586+
if (!erase_last_char('\n')) { continue; }
1587+
if (!erase_last_char('\r')) { continue; }
1588+
1589+
// Blank line indicates end of headers.
1590+
if (line_reader.ptr() == end) { break; }
1591+
1592+
while (erase_last_char(' ') || erase_last_char('\t')) {}
15551593
std::cmatch m;
1556-
if (std::regex_match(line_reader.ptr(), m, re)) {
1594+
if (std::regex_match(line_reader.ptr(), end, m, re)) {
15571595
auto key = std::string(m[1]);
15581596
auto val = std::string(m[2]);
15591597
headers.emplace(key, val);
@@ -3167,22 +3205,7 @@ inline Client::~Client() {}
31673205
inline bool Client::is_valid() const { return true; }
31683206

31693207
inline socket_t Client::create_client_socket() const {
3170-
return detail::create_socket(
3171-
host_.c_str(), port_, [=](socket_t sock, struct addrinfo &ai) -> bool {
3172-
detail::set_nonblocking(sock, true);
3173-
3174-
auto ret = connect(sock, ai.ai_addr, static_cast<int>(ai.ai_addrlen));
3175-
if (ret < 0) {
3176-
if (detail::is_connection_error() ||
3177-
!detail::wait_until_socket_is_ready(sock, timeout_sec_, 0)) {
3178-
detail::close_socket(sock);
3179-
return false;
3180-
}
3181-
}
3182-
3183-
detail::set_nonblocking(sock, false);
3184-
return true;
3185-
});
3208+
return detail::create_client_socket(host_.c_str(), port_, timeout_sec_);
31863209
}
31873210

31883211
inline bool Client::read_response_line(Stream &strm, Response &res) {

test/test.cc

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,89 @@ TEST_F(ServerTest, MultipartFormDataGzip) {
17661766
}
17671767
#endif
17681768

1769+
// Sends a raw request to a server listening at HOST:PORT.
1770+
static bool send_request(time_t read_timeout_sec, const std::string& req) {
1771+
auto client_sock =
1772+
detail::create_client_socket(HOST, PORT, /*timeout_sec=*/5);
1773+
1774+
if (client_sock == INVALID_SOCKET) { return false; }
1775+
1776+
return detail::process_and_close_socket(
1777+
true, client_sock, 1, read_timeout_sec, 0,
1778+
[&](Stream& strm, bool /*last_connection*/,
1779+
bool &/*connection_close*/) -> bool {
1780+
if (req.size() !=
1781+
static_cast<size_t>(strm.write(req.data(), req.size()))) {
1782+
return false;
1783+
}
1784+
1785+
char buf[512];
1786+
1787+
detail::stream_line_reader line_reader(strm, buf, sizeof(buf));
1788+
while (line_reader.getline()) {}
1789+
return true;
1790+
});
1791+
}
1792+
1793+
TEST(ServerRequestParsingTest, TrimWhitespaceFromHeaderValues) {
1794+
Server svr;
1795+
std::string header_value;
1796+
svr.Get("/validate-ws-in-headers",
1797+
[&](const Request &req, Response &res) {
1798+
header_value = req.get_header_value("foo");
1799+
res.set_content("ok", "text/plain");
1800+
});
1801+
1802+
thread t = thread([&] { svr.listen(HOST, PORT); });
1803+
while (!svr.is_running()) {
1804+
msleep(1);
1805+
}
1806+
1807+
// Only space and horizontal tab are whitespace. Make sure other whitespace-
1808+
// like characters are not treated the same - use vertical tab and escape.
1809+
const std::string req =
1810+
"GET /validate-ws-in-headers HTTP/1.1\r\n"
1811+
"foo: \t \v bar \e\t \r\n"
1812+
"Connection: close\r\n"
1813+
"\r\n";
1814+
1815+
ASSERT_TRUE(send_request(5, req));
1816+
svr.stop();
1817+
t.join();
1818+
EXPECT_EQ(header_value, "\v bar \e");
1819+
}
1820+
1821+
TEST(ServerRequestParsingTest, ReadHeadersRegexComplexity) {
1822+
Server svr;
1823+
svr.Get("/hi",
1824+
[&](const Request & /*req*/, Response &res) {
1825+
res.set_content("ok", "text/plain");
1826+
});
1827+
1828+
// Server read timeout must be longer than the client read timeout for the
1829+
// bug to reproduce, probably to force the server to process a request
1830+
// without a trailing blank line.
1831+
const time_t client_read_timeout_sec = 1;
1832+
svr.set_read_timeout(client_read_timeout_sec + 1, 0);
1833+
bool listen_thread_ok = false;
1834+
thread t = thread([&] { listen_thread_ok = svr.listen(HOST, PORT); });
1835+
while (!svr.is_running()) {
1836+
msleep(1);
1837+
}
1838+
1839+
// A certain header line causes an exception if the header property is parsed
1840+
// naively with a single regex. This occurs with libc++ but not libstdc++.
1841+
const std::string req =
1842+
"GET /hi HTTP/1.1\r\n"
1843+
" : "
1844+
" ";
1845+
1846+
ASSERT_TRUE(send_request(client_read_timeout_sec, req));
1847+
svr.stop();
1848+
t.join();
1849+
EXPECT_TRUE(listen_thread_ok);
1850+
}
1851+
17691852
class ServerTestWithAI_PASSIVE : public ::testing::Test {
17701853
protected:
17711854
ServerTestWithAI_PASSIVE()

0 commit comments

Comments
 (0)