Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -866,10 +866,16 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
if (sep != std::string::npos) {
uc.scheme = url.substr(0, sep);

// Scheme must be [a-z]+ only
// RFC 3986 section 3.1: scheme is case-insensitive. Store the lowercase
// form so later comparisons against "http" / "https" / "ws" / "wss" accept
// "HTTP://" and "Https://". Digits and other bytes are still rejected.
if (uc.scheme.empty()) { return false; }
for (auto c : uc.scheme) {
if (c < 'a' || c > 'z') { return false; }
for (auto &c : uc.scheme) {
if (c >= 'A' && c <= 'Z') {
c = static_cast<char>(c - 'A' + 'a');
} else if (c < 'a' || c > 'z') {
return false;
}
}

pos = sep + 3;
Expand Down
49 changes: 48 additions & 1 deletion test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6612,6 +6612,41 @@ TEST_F(ServerTest, PostMethod303Redirect) {
EXPECT_EQ("/2", res->location);
}

TEST(RedirectLocationTest, FollowsUppercaseScheme) {
// "HTTP" is the same scheme as "http". Rejecting it used to fail the whole
// Result with Error::Unknown and drop the response that had already arrived.
Server svr;
bool hit = false;
int port = 0;

svr.Get("/scheme", [&](const Request & /*req*/, Response &res) {
res.status = StatusCode::Found_302;
res.set_header("Location",
"HTTP://127.0.0.1:" + std::to_string(port) + "/b");
});
svr.Get("/b", [&](const Request & /*req*/, Response &res) {
hit = true;
res.set_content("reached", "text/plain");
});

port = svr.bind_to_any_port("127.0.0.1");
auto thread = std::thread([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();

Client cli("127.0.0.1", port);
cli.set_follow_location(true);
auto res = cli.Get("/scheme");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("reached", res->body);
EXPECT_TRUE(hit);
}

TEST_F(ServerTest, UserDefinedMIMETypeMapping) {
auto res = cli_.Get("/dir/test.abcde");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
Expand Down Expand Up @@ -18249,9 +18284,21 @@ TEST(ParseUrlTest, VariousPatterns) {
EXPECT_TRUE(uc.host.empty());
EXPECT_TRUE(uc.path.empty());
}
{
// Scheme is case-insensitive (RFC 3986). The stored form is lowercase so
// callers can keep comparing against "http" / "https".
detail::UrlComponents uc;
ASSERT_TRUE(detail::parse_url("HTTP://example.com/path", uc));
EXPECT_EQ("http", uc.scheme);
EXPECT_EQ("example.com", uc.host);
EXPECT_EQ("/path", uc.path);
}
{
detail::UrlComponents uc;
ASSERT_FALSE(detail::parse_url("HTTP://example.com/path", uc));
ASSERT_TRUE(detail::parse_url("Https://example.com/Path", uc));
EXPECT_EQ("https", uc.scheme);
EXPECT_EQ("example.com", uc.host);
EXPECT_EQ("/Path", uc.path);
}
{
detail::UrlComponents uc;
Expand Down
Loading