mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-09-30 12:42:26 +07:00
Resolve relative Location references on redirect (#2586)
This commit is contained in:
@@ -939,6 +939,48 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
inline std::string resolve_relative_location(const std::string &location,
|
||||
const std::string &base) {
|
||||
if (location.empty() || location[0] == '/') { return location; }
|
||||
|
||||
// A ':' in the first segment means the value has a scheme.
|
||||
if (location.find(':') < location.find_first_of("/?#")) { return location; }
|
||||
|
||||
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; }
|
||||
|
||||
auto base_path = base.substr(0, base.find_first_of("?#"));
|
||||
if (location[0] == '?') { return base_path + location; }
|
||||
|
||||
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
|
||||
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + 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);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
enum class SSLVerifierResponse {
|
||||
@@ -15506,7 +15548,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
if (location.empty()) { return false; }
|
||||
|
||||
detail::UrlComponents uc;
|
||||
if (!detail::parse_url(location, uc)) { return false; }
|
||||
if (!detail::parse_url(detail::resolve_relative_location(location, req.path),
|
||||
uc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only follow http/https redirects
|
||||
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
||||
|
||||
@@ -17281,6 +17281,93 @@ TEST(RedirectTest, RedirectWithPlusInPath) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, ResolveRelativeLocation) {
|
||||
// Examples from RFC 3986 section 5.4, base "http://a/b/c/d;p?q".
|
||||
const std::vector<std::pair<std::string, std::string>> cases = {
|
||||
{"g", "/b/c/g"},
|
||||
{"./g", "/b/c/g"},
|
||||
{"g/", "/b/c/g/"},
|
||||
{"?y", "/b/c/d;p?y"},
|
||||
{"g?y", "/b/c/g?y"},
|
||||
{"#s", "/b/c/d;p?q#s"},
|
||||
{"g#s", "/b/c/g#s"},
|
||||
{"g?y#s", "/b/c/g?y#s"},
|
||||
{";x", "/b/c/;x"},
|
||||
{"g;x", "/b/c/g;x"},
|
||||
{".", "/b/c/"},
|
||||
{"./", "/b/c/"},
|
||||
{"..", "/b/"},
|
||||
{"../", "/b/"},
|
||||
{"../g", "/b/g"},
|
||||
{"../..", "/"},
|
||||
{"../../", "/"},
|
||||
{"../../g", "/g"},
|
||||
{"../../../g", "/g"},
|
||||
{"g.", "/b/c/g."},
|
||||
{".g", "/b/c/.g"},
|
||||
{"g..", "/b/c/g.."},
|
||||
{"..g", "/b/c/..g"},
|
||||
{"./../g", "/b/g"},
|
||||
{"./g/.", "/b/c/g/"},
|
||||
{"g/./h", "/b/c/g/h"},
|
||||
{"g/../h", "/b/c/h"},
|
||||
{"g;x=1/./y", "/b/c/g;x=1/y"},
|
||||
{"g;x=1/../y", "/b/c/y"},
|
||||
{"g?y/./x", "/b/c/g?y/./x"},
|
||||
{"g#s/../x", "/b/c/g#s/../x"},
|
||||
// Not relative-path references, so left for parse_url.
|
||||
{"/g", "/g"},
|
||||
{"//g", "//g"},
|
||||
{"http://g/x", "http://g/x"},
|
||||
{"g:h", "g:h"},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
EXPECT_EQ(c.second,
|
||||
detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
|
||||
<< c.first;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/dir/page", [](const Request &req, Response &res) {
|
||||
res.set_redirect(req.get_param_value("to"));
|
||||
});
|
||||
|
||||
svr.Get("/dir/next", [](const Request &req, Response &res) {
|
||||
res.set_content(req.target, "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/other", [](const Request &req, Response &res) {
|
||||
res.set_content(req.target, "text/plain");
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
const std::vector<std::pair<std::string, std::string>> cases = {
|
||||
{"next", "/dir/next"},
|
||||
{"./next?x=1", "/dir/next?x=1"},
|
||||
{"../other", "/other"},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
Client cli(HOST, PORT);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/dir/page?to=" + encode_query_component(c.first));
|
||||
ASSERT_TRUE(res) << c.first << ": " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status) << c.first;
|
||||
EXPECT_EQ(c.second, res->body) << c.first;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, Issue2185_Online) {
|
||||
SSLClient client("github.com");
|
||||
|
||||
Reference in New Issue
Block a user