diff --git a/cpp/src/arrow/flight/CMakeLists.txt b/cpp/src/arrow/flight/CMakeLists.txt index 86e3c510ebb..835a56cdfa2 100644 --- a/cpp/src/arrow/flight/CMakeLists.txt +++ b/cpp/src/arrow/flight/CMakeLists.txt @@ -118,6 +118,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_BACKUP}") # protobuf-internal.cc set(ARROW_FLIGHT_SRCS client.cc + client_cookie_middleware.cc client_header_internal.cc internal.cc protocol_internal.cc diff --git a/cpp/src/arrow/flight/client_cookie_middleware.cc b/cpp/src/arrow/flight/client_cookie_middleware.cc new file mode 100644 index 00000000000..145705e9715 --- /dev/null +++ b/cpp/src/arrow/flight/client_cookie_middleware.cc @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/flight/client_cookie_middleware.h" +#include "arrow/flight/client_header_internal.h" +#include "arrow/util/value_parsing.h" + +namespace arrow { +namespace flight { + +/// \brief Client-side middleware for sending/receiving HTTP style cookies. +class ClientCookieMiddlewareFactory : public ClientMiddlewareFactory { + public: + void StartCall(const CallInfo& info, std::unique_ptr* middleware) { + ARROW_UNUSED(info); + *middleware = std::unique_ptr(new ClientCookieMiddleware(*this)); + } + + private: + class ClientCookieMiddleware : public ClientMiddleware { + public: + explicit ClientCookieMiddleware(ClientCookieMiddlewareFactory& factory) + : factory_(factory) {} + + void SendingHeaders(AddCallHeaders* outgoing_headers) override { + const std::string& cookie_string = factory_.cookie_cache_.GetValidCookiesAsString(); + if (!cookie_string.empty()) { + outgoing_headers->AddHeader("cookie", cookie_string); + } + } + + void ReceivedHeaders(const CallHeaders& incoming_headers) override { + factory_.cookie_cache_.UpdateCachedCookies(incoming_headers); + } + + void CallCompleted(const Status& status) override {} + + private: + ClientCookieMiddlewareFactory& factory_; + }; + + // Cookie cache has mutex to protect itself. + internal::CookieCache cookie_cache_; +}; + +std::shared_ptr GetCookieFactory() { + return std::make_shared(); +} + +} // namespace flight +} // namespace arrow diff --git a/cpp/src/arrow/flight/client_cookie_middleware.h b/cpp/src/arrow/flight/client_cookie_middleware.h new file mode 100644 index 00000000000..6a56a632dfb --- /dev/null +++ b/cpp/src/arrow/flight/client_cookie_middleware.h @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Middleware implementation for sending and receiving HTTP cookies. + +#pragma once + +#include + +#include "arrow/flight/client_middleware.h" + +namespace arrow { +namespace flight { + +/// \brief Returns a ClientMiddlewareFactory that handles sending and receiving cookies. +ARROW_FLIGHT_EXPORT std::shared_ptr GetCookieFactory(); + +} // namespace flight +} // namespace arrow diff --git a/cpp/src/arrow/flight/client_header_internal.cc b/cpp/src/arrow/flight/client_header_internal.cc index 2112b41f72f..2669b029bf5 100644 --- a/cpp/src/arrow/flight/client_header_internal.cc +++ b/cpp/src/arrow/flight/client_header_internal.cc @@ -21,22 +21,269 @@ #include "arrow/flight/client_header_internal.h" #include "arrow/flight/client.h" #include "arrow/flight/client_auth.h" +#include "arrow/flight/platform.h" #include "arrow/util/base64.h" #include "arrow/util/make_unique.h" +#include "arrow/util/string.h" +#include "arrow/util/uri.h" +#include "arrow/util/value_parsing.h" + +#ifdef _WIN32 +#define strcasecmp stricmp +#endif #include #include +#include +#include #include +#include #include const char kAuthHeader[] = "authorization"; const char kBearerPrefix[] = "Bearer "; const char kBasicPrefix[] = "Basic "; +const char kCookieExpiresFormat[] = "%d %m %Y %H:%M:%S"; namespace arrow { namespace flight { namespace internal { +using CookiePair = arrow::util::optional>; +using CookieHeaderPair = + const std::pair&; + +bool CaseInsensitiveComparator::operator()(const std::string& lhs, + const std::string& rhs) const { + return strcasecmp(lhs.c_str(), rhs.c_str()) < 0; +} + +size_t CaseInsensitiveHash::operator()(const std::string& key) const { + std::string upper_string = key; + std::transform(upper_string.begin(), upper_string.end(), upper_string.begin(), + ::toupper); + return std::hash{}(upper_string); +} + +Cookie Cookie::parse(const arrow::util::string_view& cookie_header_value) { + // Parse the cookie string. If the cookie has an expiration, record it. + // If the cookie has a max-age, calculate the current time + max_age and set that as + // the expiration. + Cookie cookie; + cookie.has_expiry_ = false; + std::string cookie_value_str(cookie_header_value); + + // There should always be a first match which should be the name and value of the + // cookie. + std::string::size_type pos = 0; + CookiePair cookie_pair = ParseCookieAttribute(cookie_value_str, &pos); + if (!cookie_pair.has_value()) { + // No cookie found. Mark the output cookie as expired. + cookie.has_expiry_ = true; + cookie.expiration_time_ = std::chrono::system_clock::now(); + } else { + cookie.cookie_name_ = cookie_pair.value().first; + cookie.cookie_value_ = cookie_pair.value().second; + } + + while (pos < cookie_value_str.size()) { + cookie_pair = ParseCookieAttribute(cookie_value_str, &pos); + if (!cookie_pair.has_value()) { + break; + } + + std::string cookie_attr_value_str = cookie_pair.value().second; + if (arrow::internal::AsciiEqualsCaseInsensitive(cookie_pair.value().first, + "max-age")) { + // Note: max-age takes precedence over expires. We don't really care about other + // attributes and will arbitrarily take the first max-age. We can stop the loop + // here. + cookie.has_expiry_ = true; + int max_age = -1; + try { + max_age = std::stoi(cookie_attr_value_str); + } catch (...) { + // stoi throws an exception when it fails, just ignore and leave max_age as -1. + } + + if (max_age <= 0) { + // Force expiration. + cookie.expiration_time_ = std::chrono::system_clock::now(); + } else { + // Max-age is in seconds. + cookie.expiration_time_ = + std::chrono::system_clock::now() + std::chrono::seconds(max_age); + } + break; + } else if (arrow::internal::AsciiEqualsCaseInsensitive(cookie_pair.value().first, + "expires")) { + cookie.has_expiry_ = true; + int64_t seconds = 0; + ConvertCookieDate(&cookie_attr_value_str); + if (arrow::internal::ParseTimestampStrptime( + cookie_attr_value_str.c_str(), cookie_attr_value_str.size(), + kCookieExpiresFormat, false, true, arrow::TimeUnit::SECOND, &seconds)) { + cookie.expiration_time_ = std::chrono::time_point( + std::chrono::seconds(seconds)); + } else { + // Force expiration. + cookie.expiration_time_ = std::chrono::system_clock::now(); + } + } + } + + return cookie; +} + +CookiePair Cookie::ParseCookieAttribute(const std::string& cookie_header_value, + std::string::size_type* start_pos) { + std::string::size_type equals_pos = cookie_header_value.find('=', *start_pos); + if (std::string::npos == equals_pos) { + // No cookie attribute. + *start_pos = std::string::npos; + return arrow::util::nullopt; + } + + std::string::size_type semi_col_pos = cookie_header_value.find(';', equals_pos); + std::string out_key = arrow::internal::TrimString( + cookie_header_value.substr(*start_pos, equals_pos - *start_pos)); + std::string out_value; + if (std::string::npos == semi_col_pos) { + // Last item - set start pos to end + out_value = arrow::internal::TrimString(cookie_header_value.substr(equals_pos + 1)); + *start_pos = std::string::npos; + } else { + out_value = arrow::internal::TrimString( + cookie_header_value.substr(equals_pos + 1, semi_col_pos - equals_pos - 1)); + *start_pos = semi_col_pos + 1; + } + + // Key/Value may be URI-encoded. + out_key = arrow::internal::UriUnescape(out_key); + out_value = arrow::internal::UriUnescape(out_value); + + // Strip outer quotes on the value. + if (out_value.size() >= 2 && out_value[0] == '"' && + out_value[out_value.size() - 1] == '"') { + out_value = out_value.substr(1, out_value.size() - 2); + } + + // Update the start position for subsequent calls to this function. + return std::make_pair(out_key, out_value); +} + +void Cookie::ConvertCookieDate(std::string* date) { + // Abbreviated months in order. + static const std::vector months = { + "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + + // The date comes in with the following format: Wed, 01 Jan 3000 22:15:36 GMT + // Symbolics are not supported by Windows parsing, so we need to convert to + // the following format: 01 01 3000 22:15:36 + + // String is currently in regular format: 'Wed, 01 Jan 3000 22:15:36 GMT' + // Start by removing comma and everything before it, then trimming space. + auto comma_loc = date->find(","); + if (comma_loc == std::string::npos) { + return; + } + *date = arrow::internal::TrimString(date->substr(comma_loc + 1)); + + // String is now in trimmed format: '01 Jan 3000 22:15:36 GMT' + // Now swap month to proper month format for Windows. + // Start by removing case sensitivity. + std::transform(date->begin(), date->end(), date->begin(), ::toupper); + + // Loop through months. + for (size_t i = 0; i < months.size(); i++) { + // Search the date for the month. + auto it = date->find(months[i]); + if (it != std::string::npos) { + // Create month integer, pad with leading zeros if required. + std::string padded_month; + if ((i + 1) < 10) { + padded_month = "0"; + } + padded_month += std::to_string(i + 1); + + // Replace symbolic month with numeric month. + date->replace(it, months[i].length(), padded_month); + + // String is now in format: '01 01 3000 22:15:36 GMT'. + break; + } + } + + // String is now in format '01 01 3000 22:15:36'. + auto gmt = date->find(" GMT"); + if (gmt == std::string::npos) { + return; + } + date->erase(gmt, 4); + + // Sometimes a semicolon is added at the end, if this is the case, remove it. + if (date->back() == ';') { + date->pop_back(); + } +} + +bool Cookie::IsExpired() const { + // Check if current-time is less than creation time. + return (has_expiry_ && (expiration_time_ <= std::chrono::system_clock::now())); +} + +std::string Cookie::AsCookieString() const { + // Return the string for the cookie as it would appear in a Cookie header. + // Keys must be wrapped in quotes depending on if this is a v1 or v2 cookie. + return cookie_name_ + "=\"" + cookie_value_ + "\""; +} + +std::string Cookie::GetName() const { return cookie_name_; } + +void CookieCache::DiscardExpiredCookies() { + for (auto it = cookies.begin(); it != cookies.end();) { + if (it->second.IsExpired()) { + it = cookies.erase(it); + } else { + ++it; + } + } +} + +void CookieCache::UpdateCachedCookies(const CallHeaders& incoming_headers) { + CookieHeaderPair header_values = incoming_headers.equal_range("set-cookie"); + const std::lock_guard guard(mutex_); + + for (auto it = header_values.first; it != header_values.second; ++it) { + const util::string_view& value = it->second; + Cookie cookie = Cookie::parse(value); + + // Cache cookies regardless of whether or not they are expired. The server may have + // explicitly sent a Set-Cookie to expire a cached cookie. + auto insertable = cookies.insert({cookie.GetName(), cookie}); + + // Force overwrite on insert collision. + if (!insertable.second) { + insertable.first->second = cookie; + } + } +} + +std::string CookieCache::GetValidCookiesAsString() { + const std::lock_guard guard(mutex_); + + DiscardExpiredCookies(); + if (cookies.empty()) { + return ""; + } + + std::string cookie_string = cookies.begin()->second.AsCookieString(); + for (auto it = (++cookies.begin()); cookies.end() != it; ++it) { + cookie_string += "; " + it->second.AsCookieString(); + } + return cookie_string; +} + /// \brief Add base64 encoded credentials to the outbound headers. /// /// \param context Context object to add the headers to. diff --git a/cpp/src/arrow/flight/client_header_internal.h b/cpp/src/arrow/flight/client_header_internal.h index 718848a5ffd..dd4498e0315 100644 --- a/cpp/src/arrow/flight/client_header_internal.h +++ b/cpp/src/arrow/flight/client_header_internal.h @@ -22,6 +22,7 @@ #include "arrow/flight/client_middleware.h" #include "arrow/result.h" +#include "arrow/util/optional.h" #ifdef GRPCPP_PP_INCLUDE #include @@ -32,10 +33,103 @@ #include #endif +#include +#include +#include +#include +#include + namespace arrow { namespace flight { namespace internal { +/// \brief Case insensitive comparator for use by cookie caching map. Cookies are not +/// case sensitive. +class ARROW_FLIGHT_EXPORT CaseInsensitiveComparator { + public: + bool operator()(const std::string& t1, const std::string& t2) const; +}; + +/// \brief Case insensitive hasher for use by cookie caching map. Cookies are not +/// case sensitive. +class ARROW_FLIGHT_EXPORT CaseInsensitiveHash { + public: + size_t operator()(const std::string& key) const; +}; + +/// \brief Class to represent a cookie. +class ARROW_FLIGHT_EXPORT Cookie { + public: + /// \brief Parse function to parse a cookie header value and return a Cookie object. + /// + /// \return Cookie object based on cookie header value. + static Cookie parse(const arrow::util::string_view& cookie_header_value); + + /// \brief Parse a cookie header string beginning at the given start_pos and identify + /// the name and value of an attribute. + /// + /// \param cookie_header_value The value of the Set-Cookie header. + /// \param[out] start_pos An input/output parameter indicating the starting position + /// of the attribute. It will store the position of the next attribute when the + /// function returns. + /// + /// \return Optional cookie key value pair. + static arrow::util::optional> ParseCookieAttribute( + const std::string& cookie_header_value, std::string::size_type* start_pos); + + /// \brief Function to fix cookie format date string so it is accepted by Windows + /// + /// parsers. + /// \param date Date to fix. + static void ConvertCookieDate(std::string* date); + + /// \brief Function to check if the cookie has expired. + /// + /// \return Returns true if the cookie has expired. + bool IsExpired() const; + + /// \brief Function to get cookie as a string. + /// + /// \return Cookie as a string. + std::string AsCookieString() const; + + /// \brief Function to get name of the cookie as a string. + /// + /// \return Name of the cookie as a string. + std::string GetName() const; + + private: + std::string cookie_name_; + std::string cookie_value_; + std::chrono::time_point expiration_time_; + bool has_expiry_; +}; + +/// \brief Class to handle updating a cookie cache. +class ARROW_FLIGHT_EXPORT CookieCache { + public: + /// \brief Updates the cache of cookies with new Set-Cookie header values. + /// + /// \param incoming_headers The range representing header values. + void UpdateCachedCookies(const CallHeaders& incoming_headers); + + /// \brief Retrieve the cached cookie values as a string. This function discards + /// cookies that have expired. + /// + /// \return a string that can be used in a Cookie header representing the cookies that + /// have been cached. + std::string GetValidCookiesAsString(); + + private: + /// \brief Removes cookies that are marked as expired from the cache. + void DiscardExpiredCookies(); + + // Mutex must be used to protect cookie cache. + std::mutex mutex_; + std::unordered_map + cookies; +}; + /// \brief Add basic authentication header key value pair to context. /// /// \param context grpc context variable to add header to. diff --git a/cpp/src/arrow/flight/flight_test.cc b/cpp/src/arrow/flight/flight_test.cc index 2868f84e7c9..6eaf8cd5069 100644 --- a/cpp/src/arrow/flight/flight_test.cc +++ b/cpp/src/arrow/flight/flight_test.cc @@ -39,11 +39,13 @@ #include "arrow/util/base64.h" #include "arrow/util/logging.h" #include "arrow/util/make_unique.h" +#include "arrow/util/string.h" #ifdef GRPCPP_GRPCPP_H #error "gRPC headers should not be in public API" #endif +#include "arrow/flight/client_cookie_middleware.h" #include "arrow/flight/client_header_internal.h" #include "arrow/flight/internal.h" #include "arrow/flight/middleware_internal.h" @@ -1186,6 +1188,139 @@ class TestBasicHeaderAuthMiddleware : public ::testing::Test { std::shared_ptr bearer_middleware_; }; +// This test keeps an internal cookie cache and compares that with the middleware. +class TestCookieMiddleware : public ::testing::Test { + public: + // Setup function creates middleware factory and starts it up. + void SetUp() { + factory_ = GetCookieFactory(); + CallInfo callInfo; + factory_->StartCall(callInfo, &middleware_); + } + + // Function to add incoming cookies to middleware and validate them. + void AddAndValidate(const std::string& incoming_cookie) { + // Add cookie + CallHeaders call_headers; + call_headers.insert(std::make_pair(arrow::util::string_view("set-cookie"), + arrow::util::string_view(incoming_cookie))); + middleware_->ReceivedHeaders(call_headers); + expected_cookie_cache_.UpdateCachedCookies(call_headers); + + // Get cookie from middleware. + TestCallHeaders add_call_headers; + middleware_->SendingHeaders(&add_call_headers); + const std::string actual_cookies = add_call_headers.GetCookies(); + + // Validate cookie + const std::string expected_cookies = expected_cookie_cache_.GetValidCookiesAsString(); + const std::vector split_expected_cookies = + SplitCookies(expected_cookies); + const std::vector split_actual_cookies = SplitCookies(actual_cookies); + EXPECT_EQ(split_expected_cookies, split_actual_cookies); + } + + // Function to take a list of cookies and split them into a vector of individual + // cookies. This is done because the cookie cache is a map so ordering is not + // necessarily consistent. + static std::vector SplitCookies(const std::string& cookies) { + std::vector split_cookies; + std::string::size_type pos1 = 0; + std::string::size_type pos2 = 0; + while ((pos2 = cookies.find(';', pos1)) != std::string::npos) { + split_cookies.push_back( + arrow::internal::TrimString(cookies.substr(pos1, pos2 - pos1))); + pos1 = pos2 + 1; + } + if (pos1 < cookies.size()) { + split_cookies.push_back(arrow::internal::TrimString(cookies.substr(pos1))); + } + std::sort(split_cookies.begin(), split_cookies.end()); + return split_cookies; + } + + protected: + // Class to allow testing of the call headers. + class TestCallHeaders : public AddCallHeaders { + public: + TestCallHeaders() {} + ~TestCallHeaders() {} + + // Function to add cookie header. + void AddHeader(const std::string& key, const std::string& value) { + ASSERT_EQ(key, "cookie"); + outbound_cookie_ = value; + } + + // Function to get outgoing cookie. + std::string GetCookies() { return outbound_cookie_; } + + private: + std::string outbound_cookie_; + }; + + internal::CookieCache expected_cookie_cache_; + std::unique_ptr middleware_; + std::shared_ptr factory_; +}; + +// This test is used to test the parsing capabilities of the cookie framework. +class TestCookieParsing : public ::testing::Test { + public: + void VerifyParseCookie(const std::string& cookie_str, bool expired) { + internal::Cookie cookie = internal::Cookie::parse(cookie_str); + EXPECT_EQ(expired, cookie.IsExpired()); + } + + void VerifyCookieName(const std::string& cookie_str, const std::string& name) { + internal::Cookie cookie = internal::Cookie::parse(cookie_str); + EXPECT_EQ(name, cookie.GetName()); + } + + void VerifyCookieString(const std::string& cookie_str, + const std::string& cookie_as_string) { + internal::Cookie cookie = internal::Cookie::parse(cookie_str); + EXPECT_EQ(cookie_as_string, cookie.AsCookieString()); + } + + void VerifyCookieDateConverson(std::string date, const std::string& converted_date) { + internal::Cookie::ConvertCookieDate(&date); + EXPECT_EQ(converted_date, date); + } + + void VerifyCookieAttributeParsing( + const std::string cookie_str, std::string::size_type start_pos, + const util::optional> cookie_attribute, + const std::string::size_type start_pos_after) { + util::optional> attr = + internal::Cookie::ParseCookieAttribute(cookie_str, &start_pos); + + if (cookie_attribute == util::nullopt) { + EXPECT_EQ(cookie_attribute, attr); + } else { + EXPECT_EQ(cookie_attribute.value(), attr.value()); + } + EXPECT_EQ(start_pos_after, start_pos); + } + + void AddCookieVerifyCache(const std::vector& cookies, + const std::string& expected_cookies) { + internal::CookieCache cookie_cache; + for (auto& cookie : cookies) { + // Add cookie + CallHeaders call_headers; + call_headers.insert(std::make_pair(arrow::util::string_view("set-cookie"), + arrow::util::string_view(cookie))); + cookie_cache.UpdateCachedCookies(call_headers); + } + const std::string actual_cookies = cookie_cache.GetValidCookiesAsString(); + const std::vector actual_split_cookies = + TestCookieMiddleware::SplitCookies(actual_cookies); + const std::vector expected_split_cookies = + TestCookieMiddleware::SplitCookies(expected_cookies); + } +}; + TEST_F(TestErrorMiddleware, TestMetadata) { Action action; std::unique_ptr stream; @@ -2373,5 +2508,139 @@ TEST_F(TestBasicHeaderAuthMiddleware, ValidCredentials) { RunValidClientAuth(); TEST_F(TestBasicHeaderAuthMiddleware, InvalidCredentials) { RunInvalidClientAuth(); } +TEST_F(TestCookieMiddleware, BasicParsing) { + AddAndValidate("id1=1; foo=bar;"); + AddAndValidate("id1=1; foo=bar"); + AddAndValidate("id2=2;"); + AddAndValidate("id4=\"4\""); + AddAndValidate("id5=5; foo=bar; baz=buz;"); +} + +TEST_F(TestCookieMiddleware, Overwrite) { + AddAndValidate("id0=0"); + AddAndValidate("id0=1"); + AddAndValidate("id1=0"); + AddAndValidate("id1=1"); + AddAndValidate("id1=1"); + AddAndValidate("id1=10"); + AddAndValidate("id=3"); + AddAndValidate("id=0"); + AddAndValidate("id=0"); +} + +TEST_F(TestCookieMiddleware, MaxAge) { + AddAndValidate("id0=0; max-age=0;"); + AddAndValidate("id1=0; max-age=-1;"); + AddAndValidate("id2=0; max-age=0"); + AddAndValidate("id3=0; max-age=-1"); + AddAndValidate("id4=0; max-age=1"); + AddAndValidate("id5=0; max-age=1"); + AddAndValidate("id4=0; max-age=0"); + AddAndValidate("id5=0; max-age=0"); +} + +TEST_F(TestCookieMiddleware, Expires) { + AddAndValidate("id0=0; expires=0, 0 0 0 0:0:0 GMT;"); + AddAndValidate("id0=0; expires=0, 0 0 0 0:0:0 GMT"); + AddAndValidate("id0=0; expires=Fri, 22 Dec 2017 22:15:36 GMT;"); + AddAndValidate("id0=0; expires=Fri, 22 Dec 2017 22:15:36 GMT"); + AddAndValidate("id0=0; expires=Fri, 01 Jan 2038 22:15:36 GMT;"); + AddAndValidate("id1=0; expires=Fri, 01 Jan 2038 22:15:36 GMT"); + AddAndValidate("id0=0; expires=Fri, 22 Dec 2017 22:15:36 GMT;"); + AddAndValidate("id1=0; expires=Fri, 22 Dec 2017 22:15:36 GMT"); +} + +TEST_F(TestCookieParsing, Expired) { + VerifyParseCookie("id0=0; expires=Fri, 22 Dec 2017 22:15:36 GMT;", true); + VerifyParseCookie("id1=0; max-age=-1;", true); + VerifyParseCookie("id0=0; max-age=0;", true); +} + +TEST_F(TestCookieParsing, Invalid) { + VerifyParseCookie("id1=0; expires=0, 0 0 0 0:0:0 GMT;", true); + VerifyParseCookie("id1=0; expires=Fri, 01 FOO 2038 22:15:36 GMT", true); + VerifyParseCookie("id1=0; expires=foo", true); + VerifyParseCookie("id1=0; expires=", true); + VerifyParseCookie("id1=0; max-age=FOO", true); + VerifyParseCookie("id1=0; max-age=", true); +} + +TEST_F(TestCookieParsing, NoExpiry) { + VerifyParseCookie("id1=0;", false); + VerifyParseCookie("id1=0; noexpiry=Fri, 01 Jan 2038 22:15:36 GMT", false); + VerifyParseCookie("id1=0; noexpiry=\"Fri, 01 Jan 2038 22:15:36 GMT\"", false); + VerifyParseCookie("id1=0; nomax-age=-1", false); + VerifyParseCookie("id1=0; nomax-age=\"-1\"", false); + VerifyParseCookie("id1=0; randomattr=foo", false); +} + +TEST_F(TestCookieParsing, NotExpired) { + VerifyParseCookie("id5=0; max-age=1", false); + VerifyParseCookie("id0=0; expires=Fri, 01 Jan 2038 22:15:36 GMT;", false); +} + +TEST_F(TestCookieParsing, GetName) { + VerifyCookieName("id1=1; foo=bar;", "id1"); + VerifyCookieName("id1=1; foo=bar", "id1"); + VerifyCookieName("id2=2;", "id2"); + VerifyCookieName("id4=\"4\"", "id4"); + VerifyCookieName("id5=5; foo=bar; baz=buz;", "id5"); +} + +TEST_F(TestCookieParsing, ToString) { + VerifyCookieString("id1=1; foo=bar;", "id1=\"1\""); + VerifyCookieString("id1=1; foo=bar", "id1=\"1\""); + VerifyCookieString("id2=2;", "id2=\"2\""); + VerifyCookieString("id4=\"4\"", "id4=\"4\""); + VerifyCookieString("id5=5; foo=bar; baz=buz;", "id5=\"5\""); +} + +TEST_F(TestCookieParsing, DateConversion) { + VerifyCookieDateConverson("Mon, 01 jan 2038 22:15:36 GMT;", "01 01 2038 22:15:36"); + VerifyCookieDateConverson("TUE, 10 Feb 2038 22:15:36 GMT", "10 02 2038 22:15:36"); + VerifyCookieDateConverson("WED, 20 MAr 2038 22:15:36 GMT;", "20 03 2038 22:15:36"); + VerifyCookieDateConverson("thu, 15 APR 2038 22:15:36 GMT", "15 04 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 30 mAY 2038 22:15:36 GMT;", "30 05 2038 22:15:36"); + VerifyCookieDateConverson("Sat, 03 juN 2038 22:15:36 GMT", "03 06 2038 22:15:36"); + VerifyCookieDateConverson("Sun, 01 JuL 2038 22:15:36 GMT;", "01 07 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 06 aUg 2038 22:15:36 GMT", "06 08 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 01 SEP 2038 22:15:36 GMT;", "01 09 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 01 OCT 2038 22:15:36 GMT", "01 10 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 01 Nov 2038 22:15:36 GMT;", "01 11 2038 22:15:36"); + VerifyCookieDateConverson("Fri, 01 deC 2038 22:15:36 GMT", "01 12 2038 22:15:36"); + VerifyCookieDateConverson("", ""); + VerifyCookieDateConverson("Fri, 01 INVALID 2038 22:15:36 GMT;", + "01 INVALID 2038 22:15:36"); +} + +TEST_F(TestCookieParsing, ParseCookieAttribute) { + VerifyCookieAttributeParsing("", 0, util::nullopt, std::string::npos); + + std::string cookie_string = "attr0=0; attr1=1; attr2=2; attr3=3"; + auto attr_length = std::string("attr0=0;").length(); + std::string::size_type start_pos = 0; + VerifyCookieAttributeParsing(cookie_string, start_pos, std::make_pair("attr0", "0"), + cookie_string.find("attr0=0;") + attr_length); + VerifyCookieAttributeParsing(cookie_string, (start_pos += (attr_length + 1)), + std::make_pair("attr1", "1"), + cookie_string.find("attr1=1;") + attr_length); + VerifyCookieAttributeParsing(cookie_string, (start_pos += (attr_length + 1)), + std::make_pair("attr2", "2"), + cookie_string.find("attr2=2;") + attr_length); + VerifyCookieAttributeParsing(cookie_string, (start_pos += (attr_length + 1)), + std::make_pair("attr3", "3"), std::string::npos); + VerifyCookieAttributeParsing(cookie_string, (start_pos += (attr_length - 1)), + util::nullopt, std::string::npos); + VerifyCookieAttributeParsing(cookie_string, std::string::npos, util::nullopt, + std::string::npos); +} + +TEST_F(TestCookieParsing, CookieCache) { + AddCookieVerifyCache({"id0=0;"}, ""); + AddCookieVerifyCache({"id0=0;", "id0=1;"}, "id0=\"1\""); + AddCookieVerifyCache({"id0=0;", "id1=1;"}, "id0=\"0\"; id1=\"1\""); + AddCookieVerifyCache({"id0=0;", "id1=1;", "id2=2"}, "id0=\"0\"; id1=\"1\"; id2=\"2\""); +} + } // namespace flight } // namespace arrow diff --git a/cpp/src/arrow/util/uri.cc b/cpp/src/arrow/util/uri.cc index f48a8cb6ed1..35c6b898177 100644 --- a/cpp/src/arrow/util/uri.cc +++ b/cpp/src/arrow/util/uri.cc @@ -55,15 +55,6 @@ bool IsDriveSpec(const util::string_view s) { } #endif -std::string UriUnescape(const util::string_view s) { - std::string result(s); - if (!result.empty()) { - auto end = uriUnescapeInPlaceA(&result[0]); - result.resize(end - &result[0]); - } - return result; -} - } // namespace std::string UriEscape(const std::string& s) { @@ -80,6 +71,15 @@ std::string UriEscape(const std::string& s) { return escaped; } +std::string UriUnescape(const util::string_view s) { + std::string result(s); + if (!result.empty()) { + auto end = uriUnescapeInPlaceA(&result[0]); + result.resize(end - &result[0]); + } + return result; +} + std::string UriEncodeHost(const std::string& host) { // Fairly naive check: if it contains a ':', it's IPv6 and needs // brackets, else it's OK diff --git a/cpp/src/arrow/util/uri.h b/cpp/src/arrow/util/uri.h index 480e35474a3..b4ffbb04dec 100644 --- a/cpp/src/arrow/util/uri.h +++ b/cpp/src/arrow/util/uri.h @@ -24,6 +24,7 @@ #include #include "arrow/type_fwd.h" +#include "arrow/util/string_view.h" #include "arrow/util/visibility.h" namespace arrow { @@ -91,6 +92,9 @@ class ARROW_EXPORT Uri { ARROW_EXPORT std::string UriEscape(const std::string& s); +ARROW_EXPORT +std::string UriUnescape(const arrow::util::string_view s); + /// Encode a host for use within a URI, such as "localhost", /// "127.0.0.1", or "[::1]". ARROW_EXPORT