Skip to content
Merged
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
50 changes: 30 additions & 20 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,30 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
return true;
}

// Removes "." and ".." segments from a path that starts with '/' (RFC 3986
// section 5.2.4).
inline std::string remove_dot_segments(const std::string &path) {
std::string out;
size_t i = 0;
while (i < path.size()) {
auto next = (std::min)(path.find('/', i + 1), path.size());
auto segment = path.substr(i + 1, next - i - 1);
auto is_last = next == path.size();
if (segment == "." || segment == "..") {
if (segment == "..") {
out.erase((std::min)(out.rfind('/'), out.size()));
}
if (is_last) { out += '/'; }
} else {
out += '/';
out += segment;
}
i = next;
}
if (out.empty()) { out = "/"; }
return out;
}

// Resolves a relative-path or query-only Location value against the path of
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
// references starting with '/' are returned unchanged.
Expand All @@ -1015,26 +1039,8 @@ inline std::string resolve_relative_location(const std::string &location,

// Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
std::string path;
size_t i = 0;
while (i < path_end) {
auto next = (std::min)(merged.find('/', i + 1), path_end);
auto segment = merged.substr(i + 1, next - i - 1);
auto is_last = next == path_end;
if (segment == "." || segment == "..") {
if (segment == "..") {
path.erase((std::min)(path.rfind('/'), path.size()));
}
if (is_last) { path += '/'; }
} else {
path += '/';
path += segment;
}
i = next;
}
if (path.empty()) { path = "/"; }

return path + merged.substr(path_end);
return remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end);
}

} // namespace detail
Expand Down Expand Up @@ -16405,6 +16411,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
if (next_scheme.empty()) { next_scheme = scheme; }
if (next_host.empty()) { next_host = host_; }
if (next_path.empty()) { next_path = "/"; }
// An absolute or absolute-path location can have dot segments too.
if (next_path[0] == '/') {
next_path = detail::remove_dot_segments(next_path);
}

auto path = std::move(next_path) + next_query;

Expand Down
45 changes: 45 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20593,6 +20593,51 @@ TEST(RedirectTest, ResolveRelativeLocation) {
}
}

TEST(RedirectTest, RemoveDotSegments) {
// RFC 3986 section 5.2.4
EXPECT_EQ("/a/g", detail::remove_dot_segments("/a/b/c/./../../g"));
EXPECT_EQ("/mid/6", detail::remove_dot_segments("/mid/content=5/../6"));
EXPECT_EQ("/g", detail::remove_dot_segments("/./g"));
EXPECT_EQ("/g", detail::remove_dot_segments("/../g"));
EXPECT_EQ("/", detail::remove_dot_segments("/.."));
EXPECT_EQ("/a/", detail::remove_dot_segments("/a/b/.."));
EXPECT_EQ("/a/b", detail::remove_dot_segments("/a/b"));
}

TEST(RedirectTest, AbsoluteLocationIsNormalized) {
Server svr;

svr.Get("/absolute-path", [](const Request & /*req*/, Response &res) {
res.set_redirect("/dir/../target?x=1");
});
svr.Get("/absolute-uri", [](const Request & /*req*/, Response &res) {
res.set_redirect("http://localhost:" + std::to_string(PORT) +
"/dir/./../target");
});
svr.Get("/target", [](const Request &req, Response &res) {
res.set_content(req.path, "text/plain");
});

auto th = std::thread([&]() { svr.listen("localhost", PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
th.join();
ASSERT_FALSE(svr.is_running());
});

svr.wait_until_ready();

Client cli("localhost", PORT);
cli.set_follow_location(true);

for (auto path : {"/absolute-path", "/absolute-uri"}) {
auto res = cli.Get(path);
ASSERT_TRUE(res) << path << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << path;
EXPECT_EQ("/target", res->body) << path;
}
}

TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
Server svr;

Expand Down
Loading