Просмотр исходного кода

Resolve relative Location references on redirect (#2586)

VecSzn 1 неделя назад
Родитель
Сommit
8b6ab24159
2 измененных файлов с 133 добавлено и 1 удалено
  1. 46 1
      httplib.h
  2. 87 0
      test/test.cc

+ 46 - 1
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") {

+ 87 - 0
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<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");