diff --git a/external/boost/CMakeLists.txt.boost.in b/external/boost/CMakeLists.txt.boost.in index 562b89335..d526f9278 100644 --- a/external/boost/CMakeLists.txt.boost.in +++ b/external/boost/CMakeLists.txt.boost.in @@ -24,11 +24,16 @@ include(ExternalProject) ExternalProject_Add(boost-download GIT_REPOSITORY @OLP_SDK_CPP_BOOST_URL@ GIT_TAG @OLP_SDK_CPP_BOOST_TAG@ - GIT_SUBMODULES libs/any + GIT_SUBMODULES libs/algorithm + libs/any + libs/array libs/assert + libs/concept_check libs/config + libs/container libs/container_hash libs/core + libs/date_time libs/detail libs/describe libs/format @@ -37,6 +42,7 @@ ExternalProject_Add(boost-download libs/integer libs/io libs/iterator + libs/lexical_cast libs/move libs/mpl libs/mp11 @@ -45,10 +51,12 @@ ExternalProject_Add(boost-download libs/predef libs/preprocessor libs/random + libs/range libs/serialization libs/smart_ptr libs/static_assert libs/throw_exception + libs/tokenizer libs/tti libs/type_index libs/type_traits diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClientUtils.cpp b/olp-cpp-sdk-authentication/src/AuthenticationClientUtils.cpp index ef0a9b464..dd7c45c2a 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClientUtils.cpp +++ b/olp-cpp-sdk-authentication/src/AuthenticationClientUtils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ #include #include "Constants.h" #include "ResponseFromJsonBuilder.h" +#include "Rfc1123Helper.h" #include "olp/core/http/NetworkResponse.h" #include "olp/core/http/NetworkUtils.h" #include "olp/core/logging/Log.h" @@ -58,7 +59,6 @@ constexpr auto kOauthTimestamp = "oauth_timestamp"; constexpr auto kOauthSignatureMethod = "oauth_signature_method"; constexpr auto kVersion = "1.0"; constexpr auto kHmac = "HMAC-SHA256"; -constexpr auto kLogTag = "AuthenticationClientUtils"; std::string Base64Encode(const Crypto::Sha256Digest& digest) { std::string ret = olp::utils::Base64Encode(digest.data(), digest.size()); @@ -98,52 +98,10 @@ namespace client = olp::client; constexpr auto kDate = "date"; -#ifdef _WIN32 -// Windows does not have ::strptime and ::timegm std::time_t ParseTime(const std::string& value) { - std::tm tm = {}; - std::istringstream ss(value); - ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S %z"); - return _mkgmtime(&tm); + return internal::ParseRfc1123GmtNoExceptions(value); } -#else - -std::string TrimDateHeaderValue(const std::string& value) { - const auto begin = value.find_first_not_of(" \t\r\n"); - if (begin == std::string::npos) { - return {}; - } - const auto end = value.find_last_not_of(" \t\r\n"); - return value.substr(begin, end - begin + 1); -} - -std::time_t ParseTime(const std::string& value) { - std::tm tm = {}; - const auto trimmed_value = TrimDateHeaderValue(value); - - // Use a C locale to keep RFC1123 parsing locale-independent. - // Literal "GMT" avoids platform-specific %Z behaviour. - locale_t c_locale = newlocale(LC_ALL_MASK, "C", (locale_t)0); - if (c_locale == (locale_t)0) { - OLP_SDK_LOG_WARNING(kLogTag, "Failed to create C locale"); - return static_cast(-1); - } - - const auto parsed_until = ::strptime_l( - trimmed_value.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &tm, c_locale); - freelocale(c_locale); - - if (parsed_until != trimmed_value.c_str() + trimmed_value.size()) { - OLP_SDK_LOG_WARNING(kLogTag, "Timestamp is not fully parsed " << value); - return static_cast(-1); - } - - return timegm(&tm); -} - -#endif - porting::optional GetTimestampFromHeaders( const olp::http::Headers& headers) { auto it = diff --git a/olp-cpp-sdk-authentication/src/Rfc1123Helper.cpp b/olp-cpp-sdk-authentication/src/Rfc1123Helper.cpp new file mode 100644 index 000000000..545be1611 --- /dev/null +++ b/olp-cpp-sdk-authentication/src/Rfc1123Helper.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include "Rfc1123Helper.h" + +#include +#include +#include +#include + +#include +#include +#include "olp/core/logging/Log.h" + +namespace olp { +namespace authentication { +namespace { + +constexpr auto kLogTag = "Rfc1123Helper.cpp"; +constexpr auto kGmtToken = "GMT"; + +using RfcMonthType = boost::gregorian::greg_month::value_type; +using DayType = boost::gregorian::greg_day::value_type; +using MonthType = boost::gregorian::greg_month::value_type; +using YearType = boost::gregorian::greg_year::value_type; +using HourType = boost::posix_time::time_duration::hour_type; +using MinuteType = boost::posix_time::time_duration::min_type; +using SecondType = boost::posix_time::time_duration::sec_type; + +struct ParsedRfc1123DateTime { + DayType day = 0; + MonthType month = 0; + YearType year = 0; + HourType hour = 0; + MinuteType minute = 0; + SecondType second = 0; +}; + +static_assert(std::numeric_limits::max() >= 31, + "DayType must be able to store values up to 31"); +static_assert(std::numeric_limits::max() >= 12, + "MonthType must be able to store values up to 12"); +static_assert(std::numeric_limits::max() >= 9999, + "YearType must be able to store values up to 9999"); +static_assert(std::numeric_limits::max() >= 23, + "HourType must be able to store values up to 23"); +static_assert(std::numeric_limits::max() >= 59, + "MinuteType must be able to store values up to 59"); +static_assert(std::numeric_limits::max() >= 60, + "SecondType must be able to store values up to 60, accounting " + "for leap seconds"); + +std::string TrimDateHeaderValue(const std::string& value) { + const auto begin = value.find_first_not_of(" \t\r\n"); + if (begin == std::string::npos) { + return {}; + } + const auto end = value.find_last_not_of(" \t\r\n"); + return value.substr(begin, end - begin + 1); +} + +bool ParseUnsignedInteger(const std::string& token, int* result) { + if (token.empty()) { + return false; + } + + int value = 0; + for (char c : token) { + const auto ch = static_cast(c); + if (!std::isdigit(ch)) { + return false; + } + + const auto digit = static_cast(ch - '0'); + if (value > (std::numeric_limits::max() - digit) / 10) { + return false; + } + value = value * 10 + digit; + } + + *result = value; + return true; +} + +bool ParseMonthToken(const std::string& token, RfcMonthType* month) { + if (token == "Jan") { + *month = 1; + return true; + } + if (token == "Feb") { + *month = 2; + return true; + } + if (token == "Mar") { + *month = 3; + return true; + } + if (token == "Apr") { + *month = 4; + return true; + } + if (token == "May") { + *month = 5; + return true; + } + if (token == "Jun") { + *month = 6; + return true; + } + if (token == "Jul") { + *month = 7; + return true; + } + if (token == "Aug") { + *month = 8; + return true; + } + if (token == "Sep") { + *month = 9; + return true; + } + if (token == "Oct") { + *month = 10; + return true; + } + if (token == "Nov") { + *month = 11; + return true; + } + if (token == "Dec") { + *month = 12; + return true; + } + return false; +} + +bool ParseClockToken(const std::string& token, HourType* hour, + MinuteType* minute, SecondType* second) { + if (token.size() != 8 || token[2] != ':' || token[5] != ':') { + return false; + } + + int parsed_hour = 0; + int parsed_minute = 0; + int parsed_second = 0; + if (!ParseUnsignedInteger(token.substr(0, 2), &parsed_hour) || + !ParseUnsignedInteger(token.substr(3, 2), &parsed_minute) || + !ParseUnsignedInteger(token.substr(6, 2), &parsed_second)) { + return false; + } + + if (parsed_hour > 23 || parsed_minute > 59 || parsed_second > 60) { + return false; + } + + *hour = static_cast(parsed_hour); + *minute = static_cast(parsed_minute); + *second = static_cast(parsed_second); + return true; +} + +bool ParseWeekDay(std::istringstream& stream) { + std::string weekday_token; + return (stream >> weekday_token) && weekday_token.size() == 4 && + weekday_token.back() == ','; +} + +bool ParseDay(std::istringstream& stream, ParsedRfc1123DateTime* parsed) { + std::string day_token; + int day = 0; + if (!(stream >> day_token) || !ParseUnsignedInteger(day_token, &day) || + day == 0 || day > 31) { + return false; + } + + parsed->day = static_cast(day); + return true; +} + +bool ParseMonth(std::istringstream& stream, ParsedRfc1123DateTime* parsed) { + std::string month_token; + MonthType month = 0; + if (!(stream >> month_token) || !ParseMonthToken(month_token, &month)) { + return false; + } + + parsed->month = month; + return true; +} + +bool ParseYear(std::istringstream& stream, ParsedRfc1123DateTime* parsed) { + std::string year_token; + int year = 0; + if (!(stream >> year_token) || !ParseUnsignedInteger(year_token, &year) || + year < 1400 || year > 9999) { + return false; + } + + parsed->year = static_cast(year); + return true; +} + +bool ParseTimeOfDay(std::istringstream& stream, ParsedRfc1123DateTime* parsed) { + std::string clock_token; + return (stream >> clock_token) && + ParseClockToken(clock_token, &parsed->hour, &parsed->minute, + &parsed->second); +} + +bool ParseTimeZone(std::istringstream& stream) { + std::string timezone_token; + return (stream >> timezone_token) && timezone_token == kGmtToken; +} + +bool HasNoTrailingTokens(std::istringstream& stream) { + std::string trailing_token; + return !(stream >> trailing_token); +} + +bool IsValidDateTime(const ParsedRfc1123DateTime& parsed) { + const auto year_number = static_cast(parsed.year); + const auto month_number = static_cast(parsed.month); + const auto day_number = static_cast(parsed.day); + return day_number <= boost::gregorian::gregorian_calendar::end_of_month_day( + year_number, month_number); +} + +} // namespace + +namespace internal { + +std::time_t ParseRfc1123GmtNoExceptions(const std::string& value) { + const auto trimmed_value = TrimDateHeaderValue(value); + if (trimmed_value.empty()) { + OLP_SDK_LOG_WARNING_F(kLogTag, + "Failed to parse Date header '%s': value is empty " + "after trimming whitespace", + value.c_str()); + return static_cast(-1); + } + + std::istringstream stream(trimmed_value); + ParsedRfc1123DateTime parsed; + + // Expected format: 'Thu, 1 Jan 1970 00:00:00 GMT' + // Weekday & Timezone are ignored + if (!ParseWeekDay(stream) || // + !ParseDay(stream, &parsed) || // + !ParseMonth(stream, &parsed) || // + !ParseYear(stream, &parsed) || // + !ParseTimeOfDay(stream, &parsed) || // + !ParseTimeZone(stream)) { + OLP_SDK_LOG_WARNING_F(kLogTag, + "Failed to parse Date header '%s': format mismatch " + "for RFC1123 timestamp", + value.c_str()); + return static_cast(-1); + } + + if (!HasNoTrailingTokens(stream)) { + OLP_SDK_LOG_WARNING_F(kLogTag, + "Failed to parse Date header '%s': unexpected " + "trailing characters after timestamp", + value.c_str()); + return static_cast(-1); + } + + if (!IsValidDateTime(parsed)) { + OLP_SDK_LOG_WARNING_F(kLogTag, + "Failed to parse Date header '%s': parsed value is " + "not a valid date/time", + value.c_str()); + return static_cast(-1); + } + + auto date = boost::gregorian::date(parsed.year, parsed.month, parsed.day); + auto time = boost::posix_time::time_duration(parsed.hour, parsed.minute, + parsed.second); + const auto parsed_time = boost::posix_time::ptime(date, time); + const auto epoch = + boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1)); + + if (parsed_time < epoch) { + OLP_SDK_LOG_WARNING_F( + kLogTag, + "Failed to parse Date header '%s': timestamp is before Unix epoch", + value.c_str()); + return static_cast(-1); + } + + const auto seconds_since_epoch = (parsed_time - epoch).total_seconds(); + using T = boost::remove_cv_t; + if (seconds_since_epoch > + static_cast(std::numeric_limits::max())) { + OLP_SDK_LOG_WARNING_F( + kLogTag, + "Failed to parse Date header '%s': timestamp exceeds std::time_t range", + value.c_str()); + return static_cast(-1); + } + + return static_cast(seconds_since_epoch); +} + +} // namespace internal +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/src/Rfc1123Helper.h b/olp-cpp-sdk-authentication/src/Rfc1123Helper.h new file mode 100644 index 000000000..078dc3814 --- /dev/null +++ b/olp-cpp-sdk-authentication/src/Rfc1123Helper.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include + +namespace olp { +namespace authentication { +namespace internal { + +// Parses RFC1123 date string and returns time_t. In case of parsing error, +// returns -1. Handrolled parser that: 1) works on all platforms (std::get_time +// is not supported in some environments, e.g. older libstdc++) 2) does not +// throw exceptions (boost::date_time can throw on parsing errors) 3) ignores +// locale +std::time_t ParseRfc1123GmtNoExceptions(const std::string& value); + +} // namespace internal +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/tests/CMakeLists.txt b/olp-cpp-sdk-authentication/tests/CMakeLists.txt index af6997375..f4dc82a2f 100644 --- a/olp-cpp-sdk-authentication/tests/CMakeLists.txt +++ b/olp-cpp-sdk-authentication/tests/CMakeLists.txt @@ -21,6 +21,7 @@ set(OLP_AUTHENTICATION_TEST_SOURCES DecisionApiClientTest.cpp CryptoTest.cpp SignInResultImplTest.cpp + Rfc1123HelperTest.cpp ) if (ANDROID OR IOS) diff --git a/olp-cpp-sdk-authentication/tests/Rfc1123HelperTest.cpp b/olp-cpp-sdk-authentication/tests/Rfc1123HelperTest.cpp new file mode 100644 index 000000000..1fa619f0d --- /dev/null +++ b/olp-cpp-sdk-authentication/tests/Rfc1123HelperTest.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include "../src/Rfc1123Helper.h" + +#include + +#include + +namespace { + +struct ParseCase { + const char* name; + const char* input; + std::time_t expected; +}; + +constexpr auto kParseFailed = static_cast(-1); + +class Rfc1123HelperParameterizedTest + : public ::testing::TestWithParam {}; + +TEST_P(Rfc1123HelperParameterizedTest, ParsesAsExpected) { + const auto& test_case = GetParam(); + EXPECT_EQ(olp::authentication::internal::ParseRfc1123GmtNoExceptions( + test_case.input), + test_case.expected); +} + +INSTANTIATE_TEST_SUITE_P( + ParsesAndRejectsRfc1123Dates, Rfc1123HelperParameterizedTest, + ::testing::Values( + // --- Valid inputs --- + ParseCase{"UnixEpoch", "Thu, 01 Jan 1970 00:00:00 GMT", + static_cast(0)}, + ParseCase{"SurroundingWhitespace", + "\t Tue, 13 Jan 2026 22:46:05 GMT \r\n", + static_cast(1768344365)}, + ParseCase{"SingleDigitDay", "Thu, 1 Jan 1970 00:00:00 GMT", + static_cast(0)}, + ParseCase{"MaxTimeOfDay", "Thu, 01 Jan 1970 23:59:59 GMT", + static_cast(86399)}, + ParseCase{"RecentDate", "Sun, 22 Feb 2026 15:30:45 GMT", + static_cast(1771774245)}, + ParseCase{"LeapYearFeb29", "Sat, 29 Feb 2020 12:00:00 GMT", + static_cast(1582977600)}, + ParseCase{"EndOfYear", "Thu, 31 Dec 2020 23:59:59 GMT", + static_cast(1609459199)}, + ParseCase{"March", "Sun, 8 Mar 2020 23:59:59 GMT", + static_cast(1583711999)}, + ParseCase{"Birthday", "Wed, 8 Apr 2020 23:59:59 GMT", + static_cast(1586390399)}, + ParseCase{"July", "Wed, 8 Jul 2020 23:59:59 GMT", + static_cast(1594252799)}, + ParseCase{"August", "Sat, 8 Aug 2020 23:59:59 GMT", + static_cast(1596931199)}, + // --- Before Epoch --- + ParseCase{"MinValidYear", "Mon, 01 Jan 1400 00:00:00 GMT", + kParseFailed}, + // --- Empty / garbled input --- + ParseCase{"EmptyString", "", kParseFailed}, + ParseCase{"WhitespaceOnly", " \t\r\n ", kParseFailed}, + ParseCase{"GarbledInput", "not a date", kParseFailed}, + ParseCase{"RandomNumbers", "12345", kParseFailed}, + // --- Invalid month --- + ParseCase{"InvalidMonthToken", "Tue, 13 Foo 2026 22:46:05 GMT", + kParseFailed}, + // --- Invalid timezone --- + ParseCase{"InvalidTimezoneToken", "Tue, 13 Jan 2026 22:46:05 UTC", + kParseFailed}, + ParseCase{"MissingTimezone", "Tue, 13 Jan 2026 22:46:05", kParseFailed}, + // --- Invalid day in month --- + ParseCase{"InvalidDayInMonth", "Mon, 31 Feb 2025 10:11:12 GMT", + kParseFailed}, + ParseCase{"Feb29NonLeapYear", "Sat, 29 Feb 2025 12:00:00 GMT", + kParseFailed}, + ParseCase{"DayZero", "Thu, 0 Jan 1970 00:00:00 GMT", kParseFailed}, + ParseCase{"DayTooLarge", "Thu, 32 Jan 1970 00:00:00 GMT", kParseFailed}, + // --- Invalid clock values --- + ParseCase{"HourTooLarge", "Thu, 01 Jan 1970 25:00:00 GMT", + kParseFailed}, + ParseCase{"MinuteTooLarge", "Thu, 01 Jan 1970 12:60:00 GMT", + kParseFailed}, + ParseCase{"SecondTooLarge", "Thu, 01 Jan 1970 12:00:61 GMT", + kParseFailed}, + ParseCase{"MalformedClock", "Thu, 01 Jan 1970 1:2:3 GMT", kParseFailed}, + // --- Invalid year --- + ParseCase{"YearBelowMinimum", "Mon, 01 Jan 1399 00:00:00 GMT", + kParseFailed}, + ParseCase{"YearAboveMaximum", "Mon, 01 Jan 10000 00:00:00 GMT", + kParseFailed}, + // --- Pre-epoch --- + ParseCase{"PreEpoch", "Wed, 31 Dec 1969 23:59:59 GMT", kParseFailed}, + // --- Trailing garbage --- + ParseCase{"TrailingGarbage", + "Thu, 01 Jan 1970 00:00:00 GMT extra stuff", kParseFailed}, + ParseCase{"TrailingSingleToken", "Thu, 01 Jan 1970 00:00:00 GMT X", + kParseFailed}, + // --- Structural issues --- + ParseCase{"MissingCommaAfterWeekday", "Thu 01 Jan 1970 00:00:00 GMT", + kParseFailed}, + ParseCase{"WeekdayTooShort", "Th, 01 Jan 1970 00:00:00 GMT", + kParseFailed}, + ParseCase{"WeekdayTooLong", "Thurs, 01 Jan 1970 00:00:00 GMT", + kParseFailed}, + // --- Specific edge cases --- + ParseCase{"DayAsLetters", "Thu, AA Dec 2020 23:59:59 GMT", + static_cast(kParseFailed)}, + ParseCase{"NonNumericClock", "Thu, 12 Dec 2020 11:AA:10 GMT", + static_cast(kParseFailed)} + + ), + [](const ::testing::TestParamInfo& info) { + return info.param.name; + }); + +} // namespace