Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
662cb13
Fixed storing of cookies at http client's response side (without jar …
alex-aparin Jul 8, 2026
240c8ad
Reverted changes, which break API of cookies
alex-aparin Jul 16, 2026
8a6ef54
CookieJar was added to response of http client (with backward compati…
alex-aparin Jul 16, 2026
d051cc6
Data structure for CookieJar's storage was simplified
alex-aparin Jul 17, 2026
6684fc7
Storage cookies was optimized, list was removed by smallvector
alex-aparin Jul 18, 2026
42a33d3
Storage of validated cookies was refactored, extra field were removed…
alex-aparin Jul 18, 2026
f834188
Rough implementations for GetCookies/GetAnyCookie were added.
alex-aparin Jul 19, 2026
d7a4543
Runtime error for unitests was fixed.
alex-aparin Jul 19, 2026
28e828c
Tests and bugs related with domain checks were fixed
alex-aparin Jul 20, 2026
3ada078
Bug in path's prefix matching was fixed. All tests related with cooki…
alex-aparin Jul 20, 2026
a77df26
Support for merging two cookie jars was added
alex-aparin Jul 20, 2026
496bd61
Computation default-path was fixed with test. Supercookie test was added
alex-aparin Jul 20, 2026
0d958ff
Security tests were added. API was simplified. Fixed bugs for sorting…
alex-aparin Jul 20, 2026
d44acd5
Setter of cookie jar for request was added
alex-aparin Jul 21, 2026
edc6757
Fixed error for usage empty response for cookie jar. Storage was refa…
alex-aparin Jul 22, 2026
701abff
Additional tests for covering unicode and security attributes were ad…
alex-aparin Jul 22, 2026
4be2566
Verbosity of tests were reduced via additional fixture
alex-aparin Jul 23, 2026
794ceda
Extra allocations were reduced within implementation of CookieJar
alex-aparin Jul 24, 2026
8820df6
PathMatch algorithm was simplified and followed strictly for standard…
alex-aparin Jul 27, 2026
c244815
Initial version of enabling internal libcurl cookies engine was added
alex-aparin Jul 30, 2026
bfc1ec9
Tests were adapted to use libcurl embedded cookies engine
alex-aparin Jul 31, 2026
e1268b2
FindCookieValue method was implemented
alex-aparin Jul 31, 2026
c959fcf
Resolved conflict with ca04d21 commit
alex-aparin Jul 31, 2026
a67de9c
Merge branch 'develop' into cookies_fix
alex-aparin Jul 31, 2026
af3b7ec
Inproperly merged ParseSingleCookie method was fixed
alex-aparin Jul 31, 2026
6761cf2
Support for "punification" was added
alex-aparin Aug 1, 2026
9d40b60
UnicodeUrls test was enabled with some nuanсes
alex-aparin Aug 1, 2026
cb0764b
Refactoring of tests. Minor fix for client_test CookiesMap, major tes…
alex-aparin Aug 1, 2026
6fa8a9a
Fragile constructor of CookieJar was made private. Additional tests f…
alex-aparin Aug 2, 2026
afbbbb0
Workaround about blocked fs operations of psl was added
alex-aparin Aug 10, 2026
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
38 changes: 38 additions & 0 deletions core/include/userver/clients/http/cookie_jar.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

/// @file userver/clients/http/cookie_jar.hpp
/// @brief @copybrief clients::http::CookieJar
#include <string>
#include <vector>
#include <optional>

USERVER_NAMESPACE_BEGIN

namespace clients::http {

class Request;

/// @brief Storage for cookies, compliable with RFC 6265. Can be used for sending and receiving cookies on agent side.
class CookieJar final {
public:
CookieJar();
Comment thread
alex-aparin marked this conversation as resolved.
Outdated
CookieJar(std::vector<std::string>&& cookies);
~CookieJar();
CookieJar(const CookieJar&);
CookieJar(CookieJar&&);
CookieJar& operator=(const CookieJar&);
CookieJar& operator=(CookieJar&&);

/// @brief Gets ANY cookie value, associated with name. In general case, multiple cookies can be stored with the same name, order is not specified
/// @param name Name of cookie
/// @return Cookie's value
/// @warning This method has lineral complexity
std::optional<std::string> FindCookieValue(std::string_view name);
private:
friend class Request;
std::vector<std::string> cookies_;
};

} // namespace clients::http

USERVER_NAMESPACE_END
8 changes: 8 additions & 0 deletions core/include/userver/clients/http/request.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <userver/clients/dns/resolver_fwd.hpp>
#include <userver/clients/http/error.hpp>
#include <userver/clients/http/response.hpp>
#include <userver/clients/http/cookie_jar.hpp>
#include <userver/clients/http/response_future.hpp>
#include <userver/concurrent/queue.hpp>
#include <userver/crypto/certificate.hpp>
Expand Down Expand Up @@ -250,6 +251,11 @@ class Request final {
/// Cookies for request as map
Request cookies(const std::unordered_map<std::string, std::string>& cookies) &&;

/// Sets cookie jar
Request& cookies(const CookieJar& cookie_jar) &;
/// Sets cookie jar
Request cookies(const CookieJar& cookie_jar) &&;

/// Follow redirects or not. Default: follow
Request& follow_redirects(bool follow = true) &;
/// @overload
Expand Down Expand Up @@ -447,6 +453,8 @@ class Request final {
/// Returns HTTP body of a request, leaving it empty
std::string ExtractData();

CookieJar GetCookieJar();

private:
std::shared_ptr<RequestState> pimpl_;
};
Expand Down
2 changes: 1 addition & 1 deletion core/include/userver/clients/http/response.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ class Response final {

} // namespace clients::http

USERVER_NAMESPACE_END
USERVER_NAMESPACE_END
6 changes: 4 additions & 2 deletions core/include/userver/server/http/http_response_cookie.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class Cookie final {
bool IsSecure() const noexcept;
Cookie& SetSecure() noexcept;

std::chrono::system_clock::time_point Expires() const noexcept;
// Missed Expires has special semantics
std::optional<std::chrono::system_clock::time_point> Expires() const noexcept;
Cookie& SetExpires(std::chrono::system_clock::time_point value) noexcept;

bool IsPermanent() const noexcept;
Expand All @@ -54,7 +55,8 @@ class Cookie final {
const std::string& Domain() const noexcept;
Cookie& SetDomain(std::string value);

std::chrono::seconds MaxAge() const noexcept;
// Missed MaxAge has special semantics
std::optional<std::chrono::seconds> MaxAge() const noexcept;
Cookie& SetMaxAge(std::chrono::seconds value) noexcept;

std::string SameSite() const;
Expand Down
57 changes: 57 additions & 0 deletions core/src/clients/http/client_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ g3n5Bom64kOrAWOk2xcpd0Pm00o=
constexpr char
kResponse301WithHeaderPattern[] = "HTTP/1.1 301 OK\r\nConnection: close\r\nContent-Length: 0\r\n{}\r\n\r\n";


using ::testing::IsEmpty;
using ::testing::ElementsAreArray;
using ::testing::UnorderedElementsAreArray;


class RequestMethodTestData final {
public:
using Request = clients::http::Request;
Expand Down Expand Up @@ -155,6 +161,15 @@ std::optional<HttpResponse> Process100(const HttpRequest& request) {
return std::nullopt;
}

std::vector<std::string> Pairs(const clients::http::Response::CookiesMap& cookies) {
std::vector<std::string> out;
out.reserve(cookies.size());
for (const auto& cookie : cookies) {
out.push_back(cookie.first + '=' + cookie.second.Value());
}
return out;
}

struct EchoCallback {
std::shared_ptr<std::size_t> responses_200 = std::make_shared<std::size_t>(0);

Expand Down Expand Up @@ -442,6 +457,26 @@ struct CheckCookie {
}
};

struct ReturnCookies {
const std::vector<std::string> cookies;

HttpResponse operator()(const HttpRequest&) {
constexpr std::string_view prefix = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0";
std::string builder;
builder.reserve(prefix.size() * 2 + cookies.size() * 20); // magic constant to average size of cookie string
builder.append(prefix);
for (const auto& cookie : cookies) {
builder.append("\r\nSet-Cookie: ");
builder.append(cookie);
}
builder.append("\r\n\r\n");
return HttpResponse{
std::move(builder),
HttpResponse::kWriteAndClose
};
}
};

constexpr auto kTestHosts = R"(
127.0.0.2 localhost
::1 localhost
Expand Down Expand Up @@ -1126,6 +1161,28 @@ UTEST(HttpClient, Cookies) {
test({{"a", "B"}, {"A", "b"}}, {"a=B", "A=b"});
}

UTEST(HttpClient, CookiesFromServerMapAPI) {
// Without compliant CookieJar with rfc 6265, we will just check raw cookies without deduplication etc
const auto test = [](std::vector<std::string> response_cookies, std::vector<std::string> expected) {
const utest::SimpleServer http_server{ReturnCookies{response_cookies}};
auto http_client_ptr = utest::CreateHttpClient();
for (unsigned i = 0; i < kRepetitions; ++i) {
const auto response =
http_client_ptr->CreateRequest()
.get(http_server.GetBaseUrl())
.retry(1)
.verify(true)
.http_version(USERVER_NAMESPACE::http::HttpVersion::k11)
.timeout(kTimeout)
.perform();
EXPECT_TRUE(response->IsOk());
EXPECT_THAT(Pairs(response->cookies()), UnorderedElementsAreArray(expected));
}
};
test({"token=xyz789"}, {"token=xyz789"});
test({"A=1", "A=2", "FOO=BAR"}, {"A=1", "FOO=BAR"});
}

UTEST(HttpClient, DuplicateSetCookieOverwrites) {
const utest::SimpleServer http_server{[](const utest::SimpleServer::Request&) {
static const utest::SimpleServer::Response kResponse{
Expand Down
60 changes: 60 additions & 0 deletions core/src/clients/http/cookie_jar.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <userver/clients/http/cookie_jar.hpp>

#include <chrono>
#include <cstddef>
#include <string>
#include <string_view>
#include <set>
#include <utility>
#include <vector>
#include <algorithm>
#include <boost/container/small_vector.hpp>

#include <userver/utils/datetime.hpp>
#include <userver/utils/str_icase.hpp>
#include <userver/logging/log.hpp>
#include <userver/http/url.hpp>

USERVER_NAMESPACE_BEGIN

namespace clients::http {

CookieJar::CookieJar() = default;
CookieJar::~CookieJar() = default;
CookieJar::CookieJar(std::vector<std::string>&& cookies) :
cookies_(std::move(cookies)) {

}
CookieJar::CookieJar(const CookieJar&) = default;
CookieJar::CookieJar(CookieJar&&) = default;
CookieJar& CookieJar::operator=(const CookieJar&) = default;
CookieJar& CookieJar::operator=(CookieJar&&) = default;

std::optional<std::string> CookieJar::FindCookieValue(std::string_view name) {
// TODO: not optimal way to search
for (std::string_view line : cookies_) {
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
line.remove_suffix(1);
}

const auto value_pos = line.rfind('\t');
if (value_pos == std::string_view::npos || value_pos == 0) {
// A comment or a malformed line, both have nothing to look at.
continue;
}
const auto name_pos = line.rfind('\t', value_pos - 1);
if (name_pos == std::string_view::npos) {
continue;
}

// Cookie names are case-sensitive, RFC 6265, section 5.3.
if (line.substr(name_pos + 1, value_pos - name_pos - 1) == name) {
return std::string{line.substr(value_pos + 1)};
}
}
return std::nullopt;
}

} // namespace clients::http

USERVER_NAMESPACE_END
Loading
Loading