diff --git a/psicash.cpp b/psicash.cpp index 96fc99c..cd097a8 100644 --- a/psicash.cpp +++ b/psicash.cpp @@ -68,8 +68,6 @@ static constexpr const char* kLandingPageParamKey = "psicash"; static constexpr const char* kMethodGET = "GET"; static constexpr const char* kMethodPOST = "POST"; -static constexpr const char* kDateHeaderKey = "Date"; - // // PsiCash class implementation // @@ -584,7 +582,6 @@ Result PsiCash::MakeHTTPRequestWithRetry( return WrapError(req_params.error(), "BuildRequestParams failed"); } - http_result = make_http_request_fn_(*req_params); // Error state sanity check @@ -592,8 +589,8 @@ Result PsiCash::MakeHTTPRequestWithRetry( return MakeCriticalError("HTTP result code is negative but no error message provided"); } - // We just got a fresh server timestamp, so set the server time diff - auto date_header = utils::FindHeaderValue(http_result.headers, kDateHeaderKey); + // We just got a fresh server timestamp (Date header), so set the server time diff + auto date_header = utils::FindHeaderValue(http_result.headers, "Date"); if (!date_header.empty()) { datetime::DateTime server_datetime; if (server_datetime.FromRFC7231(date_header)) { @@ -603,6 +600,11 @@ Result PsiCash::MakeHTTPRequestWithRetry( // else: we're not going to raise the error } + // Store/update any cookies we received, to send in the next request. + // We're not going to cause a general error if the cookies fail to save but + // everything else is successful. + (void)user_data_->SetCookies(utils::GetCookies(http_result.headers)); + if (http_result.code < 0) { // Something happened that prevented the request from nominally succeeding. // If the native code indicates that this is a "recoverable error" (such as @@ -654,6 +656,7 @@ Result PsiCash::BuildRequestParams( params.headers = additional_headers; params.headers["Accept"] = "application/json"; params.headers["User-Agent"] = user_agent_; + params.headers["Cookie"] = user_data_->GetCookies(); if (include_auth_tokens) { params.headers["X-PsiCash-Auth"] = CommaDelimitTokens({}); diff --git a/userdata.cpp b/userdata.cpp index 04a9390..e77cd3d 100644 --- a/userdata.cpp +++ b/userdata.cpp @@ -64,6 +64,8 @@ static constexpr const char* LAST_TRANSACTION_ID = "lastTransactionID"; static const auto kLastTransactionIDPtr = kUserPtr / LAST_TRANSACTION_ID; static const char* REQUEST_METADATA = "requestMetadata"; const json::json_pointer kRequestMetadataPtr = kUserPtr / REQUEST_METADATA; // used in header, so not static +static constexpr const char* COOKIES = "cookies"; +static const auto kCookiesPtr = kUserPtr / COOKIES; // These are the possible token types. @@ -500,6 +502,18 @@ error::Error UserData::SetLocale(const std::string& v) { return PassError(datastore_.Set(kLocalePtr, v)); } +std::string UserData::GetCookies() const { + auto v = datastore_.Get(kCookiesPtr); + if (!v) { + return ""; + } + return *v; +} + +error::Error UserData::SetCookies(const std::string& v) { + return PassError(datastore_.Set(kCookiesPtr, v)); +} + json UserData::GetStashedRequestMetadata() const { SYNCHRONIZE(stashed_request_metadata_mutex_); auto stashed = stashed_request_metadata_; diff --git a/userdata.hpp b/userdata.hpp index 3aac6cd..7f5adb7 100644 --- a/userdata.hpp +++ b/userdata.hpp @@ -148,6 +148,9 @@ class UserData { std::string GetLocale() const; error::Error SetLocale(const std::string& v); + std::string GetCookies() const; + error::Error SetCookies(const std::string& v); + protected: /// Modifies the purchases in the argument. void UpdatePurchasesLocalTimeExpiry(Purchases& purchases) const; diff --git a/userdata_test.cpp b/userdata_test.cpp index deff4ec..b4d4229 100644 --- a/userdata_test.cpp +++ b/userdata_test.cpp @@ -803,6 +803,26 @@ TEST_F(TestUserData, Locale) ASSERT_EQ(v, ""); } +TEST_F(TestUserData, Cookies) +{ + UserData ud; + auto err = ud.Init(GetTempDir().c_str(), dev); + ASSERT_FALSE(err); + + auto v = ud.GetCookies(); + ASSERT_THAT(v, IsEmpty()); + + err = ud.SetCookies("x=y; a=b; m=n"); + ASSERT_FALSE(err); + v = ud.GetCookies(); + ASSERT_EQ(v, "x=y; a=b; m=n"); + + err = ud.SetCookies(""); + ASSERT_FALSE(err); + v = ud.GetCookies(); + ASSERT_EQ(v, ""); +} + TEST_F(TestUserData, Transaction) { UserData ud; diff --git a/utils.cpp b/utils.cpp index fb83a94..c1418b9 100644 --- a/utils.cpp +++ b/utils.cpp @@ -94,14 +94,38 @@ static string ToLowerASCII(const string& s) { return ss.str(); } -string FindHeaderValue(const map>& headers, const string& key) { - auto lower_key = ToLowerASCII(key); +static vector FindHeaderValues(const map>& headers, const string& key) { + const auto lower_key = ToLowerASCII(key); for (const auto& entry : headers) { if (lower_key == ToLowerASCII(entry.first)) { - return entry.second.empty() ? "" : entry.second.front(); + return entry.second; + } + } + return {}; +} + +string FindHeaderValue(const map>& headers, const string& key) { + auto vec = FindHeaderValues(headers, key); + return vec.empty() ? "" : vec.front(); +} + +string GetCookies(const map>& headers) { + // Set-Cookie header values are of the form: + // AWSALB=abcxyz; Expires=Tue, 03 May 2022 19:47:19 GMT; Path=/ + // We only care about the cookie name and the value. + + stringstream res; + bool first = true; + for (const auto& c : FindHeaderValues(headers, "Set-Cookie")) { + if (!first) { + res << "; "; } + first = false; + + auto semi = c.find_first_of(';'); + res << TrimCopy(c.substr(0, semi)); } - return ""; + return res.str(); } // Adapted from https://stackoverflow.com/a/22986486/729729 diff --git a/utils.hpp b/utils.hpp index c4f5e04..0e35836 100644 --- a/utils.hpp +++ b/utils.hpp @@ -66,6 +66,10 @@ std::string RandomID(); /// If there are multiple header values for the key, the first one is returned. std::string FindHeaderValue(const std::map>& headers, const std::string& key); +/// Returns all cookie name=values in the Set-Cookie headers in a single +/// semicolon-separated string (suitable for sending in a request Cookie header). +std::string GetCookies(const std::map>& headers); + // From https://stackoverflow.com/a/5289170/729729 /// note: delimiter cannot contain NUL characters template diff --git a/utils_test.cpp b/utils_test.cpp index 82dbeb4..808fb80 100644 --- a/utils_test.cpp +++ b/utils_test.cpp @@ -18,10 +18,12 @@ */ #include "gtest/gtest.h" +#include "gmock/gmock.h" #include "utils.hpp" using namespace std; using namespace utils; +using namespace testing; TEST(TestStringer, SingleValue) { auto s = Stringer("s"); @@ -55,4 +57,40 @@ TEST(TestFindHeaderValue, Simple) { headers = {{"a", {"xyz"}}, {"c", {"abc", "def"}}, {"DATE", {"expected", "second"}}}; s = FindHeaderValue(headers, "Date"); ASSERT_EQ(s, "expected"); + + headers = {{"a", {"xyz"}}, {"c", {"abc", "def"}}, {"DATE", {"expected", "second"}}}; + s = FindHeaderValue(headers, "Nope"); + ASSERT_EQ(s, ""); +} + +TEST(TestGetCookies, Simple) { + map> headers; + + headers = {{"a", {"xyz"}}, {"set-COOKIE", {"AWSALBCORS=qxg5PeVRnxutG8kvdnISQvQM+PWqFzqoVZGJcyZh9c6su3O+u1121WEFwZ6DAEtVaKq6ufOzUIfAL8qRmUuSya5ODUxJOC9m3+006HBi71pSk6T88oiMgva0IOvi; Expires=Mon, 02 May 2022 20:53:02 GMT; Path=/; SameSite=None; Secure", "k1=v1", "k2=v2;"}}}; + auto v = GetCookies(headers); + ASSERT_EQ(v, "AWSALBCORS=qxg5PeVRnxutG8kvdnISQvQM+PWqFzqoVZGJcyZh9c6su3O+u1121WEFwZ6DAEtVaKq6ufOzUIfAL8qRmUuSya5ODUxJOC9m3+006HBi71pSk6T88oiMgva0IOvi; k1=v1; k2=v2"); + + headers = {{"a", {"xyz"}}}; + v = GetCookies(headers); + ASSERT_EQ(v, ""); + + headers = {{"a", {"xyz"}}, {"Set-Cookie", {}}}; + v = GetCookies(headers); + ASSERT_EQ(v, ""); + + headers = {{"a", {"xyz"}}, {"Set-Cookie", {""}}}; + v = GetCookies(headers); + ASSERT_EQ(v, ""); + + headers = {{"a", {"xyz"}}, {"Set-Cookie", {";"}}}; + v = GetCookies(headers); + ASSERT_EQ(v, ""); + + headers = {{"a", {"xyz"}}, {"Set-Cookie", {"!;!;!"}}}; + v = GetCookies(headers); + ASSERT_EQ(v, "!"); + + headers = {{"a", {"xyz"}}, {"Set-Cookie", {" x=y "}}}; + v = GetCookies(headers); + ASSERT_EQ(v, "x=y"); }