Skip to content

Commit a9e942d

Browse files
committed
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.
1 parent 4c93b97 commit a9e942d

File tree

2 files changed

+72
-17
lines changed

2 files changed

+72
-17
lines changed

httplib.h

Lines changed: 26 additions & 17 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 ]*(.+?)[\t ]*\r\n)");
15461570

15471571
const auto bufsiz = 2048;
15481572
char buf[bufsiz];
@@ -3166,22 +3190,7 @@ inline Client::~Client() {}
31663190
inline bool Client::is_valid() const { return true; }
31673191

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

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

test/test.cc

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

1769+
TEST(ServerRequestParsingTest, TrimWhitespaceFromHeaderValues) {
1770+
Server svr;
1771+
std::string header_value;
1772+
svr.Get("/validate-ws-in-headers",
1773+
[&](const Request &req, Response &res) {
1774+
header_value = req.get_header_value("foo");
1775+
res.set_content("ok", "text/plain");
1776+
});
1777+
1778+
thread t = thread([&] { svr.listen(HOST, PORT); });
1779+
while (!svr.is_running()) {
1780+
msleep(1);
1781+
}
1782+
1783+
// Only space and horizontal tab are whitespace. Make sure other whitespace-
1784+
// like characters are not treated the same - use vertical tab and escape.
1785+
auto client_sock =
1786+
detail::create_client_socket(HOST, PORT, /*timeout_sec=*/5);
1787+
ASSERT_TRUE(client_sock != INVALID_SOCKET);
1788+
const std::string req =
1789+
"GET /validate-ws-in-headers HTTP/1.1\r\n"
1790+
"foo: \t \v bar \e\t \r\n"
1791+
"Connection: close\r\n"
1792+
"\r\n";
1793+
1794+
bool process_ok = detail::process_and_close_socket(
1795+
true, client_sock, 1, 5, 0,
1796+
[&](Stream& strm, bool /*last_connection*/,
1797+
bool &/*connection_close*/) -> bool {
1798+
if (req.size() !=
1799+
static_cast<size_t>(strm.write(req.data(), req.size()))) {
1800+
return false;
1801+
}
1802+
1803+
char buf[512];
1804+
1805+
detail::stream_line_reader line_reader(strm, buf, sizeof(buf));
1806+
while (line_reader.getline()) {}
1807+
return true;
1808+
});
1809+
ASSERT_TRUE(process_ok);
1810+
svr.stop();
1811+
t.join();
1812+
EXPECT_EQ(header_value, "\v bar \e");
1813+
}
1814+
17691815
class ServerTestWithAI_PASSIVE : public ::testing::Test {
17701816
protected:
17711817
ServerTestWithAI_PASSIVE()

0 commit comments

Comments
 (0)