diff --git a/httplib.h b/httplib.h index f50e440..7c199f9 100644 --- a/httplib.h +++ b/httplib.h @@ -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") { diff --git a/test/test.cc b/test/test.cc index ebeb817..bec4cf9 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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> 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> 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");