From cf54474e385e031e4320171bc5421bae83e4547e Mon Sep 17 00:00:00 2001 From: Niels Sascha Reedijk Date: Fri, 6 May 2022 20:40:56 +0100 Subject: [PATCH] NetServices: Implement BHttpTime, parse_http_time() and format_http_time(). These utilities convert timestamp strings that are formatted according to the HTTP RFC into BDateTime objects, and vice versa. Change-Id: Ia2498944fb63d09233839f19d08f15d82a0a9685 --- docs/user/netservices/HttpTime.dox | 259 ++++++++++++++++++ headers/private/netservices2/HttpTime.h | 71 +++++ src/kits/network/libnetservices2/HttpTime.cpp | 235 ++++++++++++++++ src/kits/network/libnetservices2/Jamfile | 1 + .../net/netservices2/HttpProtocolTest.cpp | 57 +++- .../kits/net/netservices2/HttpProtocolTest.h | 1 + 6 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 docs/user/netservices/HttpTime.dox create mode 100644 headers/private/netservices2/HttpTime.h create mode 100644 src/kits/network/libnetservices2/HttpTime.cpp diff --git a/docs/user/netservices/HttpTime.dox b/docs/user/netservices/HttpTime.dox new file mode 100644 index 0000000000..33fb30479f --- /dev/null +++ b/docs/user/netservices/HttpTime.dox @@ -0,0 +1,259 @@ +/* + * Copyright 2022 Haiku, Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Niels Sascha Reedijk, niels.reedijk@gmail.com + * + * Corresponds to: + * headers/private/netservices2/HttpTime.h hrev????? + * src/kits/network/libnetservices2/HttpTime.cpp hrev????? + */ + + +#if __cplusplus >= 201703L + + +namespace BPrivate { + +namespace Network { + + +/*! + \file HttpTime.h + \ingroup netservices + \brief Provides tools to parse and format HTTP dates. + + The HTTP protocols prescribe that each HTTP response should have a \c Date header field with a + timestamp the response was generated. Optionally, there are other fields that may have a + timestamp in them, such as \c Set-Cookie or \c If-Modified-Since. + + According to section 3.3 of RFC 2616, the standard date format is the format described by + RFC 1123, which updates the previous RFC 822. However, a proper implementation of a HTTP parser + may also want to support the RFC 850 format (which was obsoleted by RFC 1036) and the old + C-library standard date formatting of \c asctime(). + + Examples: + + + + +
RFC1123 / RFC822Sun, 06 Nov 1994 08:49:37 GMT
RFC850Sunday, 06-Nov-94 08:49:37 GMT
asctimeSun Nov 6 08:49:37 1994
+ + The tools in this module will make it easer to parse and format dates according to those + standards. When parsing the + + You can use \ref parse_http_time() to parse a string that contains a HTTP timestamp. You can + use \ref format_http_time() to format the HTTP time according to the prescribed format. If you + want slightly more information about parsing, or if you want to hold an intermediate + representation of the timestamp, have a look at the \ref BPrivate::Network::BHttpTime class. + + Note that when parsing a timestamp string, the tools are slightly more permissive than the + standards. For example, if the RFC 1123 timestamp does not have the GMT timezone indicator at + the end, it will still be accepted. Likewise, there is support for RFC 850 timestamps with a + 4-digit year format. When formatting a \ref BPrivate::BDateTime to a string, it will always use + prescribed representation. + + \since Haiku R1 +*/ + + +/*! + \enum BHttpTimeFormat + \brief Describes the three time formats supported by the HTTP RFC. +*/ + + +/*! + \class BHttpTime::InvalidInput + \ingroup netservices + \brief Error that indicates that a string cannot be parsed as a valid HTTP timestamp. + + \since Haiku R1 +*/ + + +/*! + \var BString BHttpTime::InvalidInput::input + \brief Copy of the original timestamp that could not be parsed. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpTime::InvalidInput::InvalidInput(const char *origin, BString input) + \brief Constructor that sets the \a origin and the invalid \a input. + + \since Haiku R1 +*/ + + +/*! + \class BHttpTime + \ingroup netservices + \brief Utility class that can parse and format HTTP Date strings. + + See the description of the module in \ref HttpTime.h for more information about HTTP + timestamps. + + Note that for quick conversions of a \ref BDateTime into a \ref BString and vice versa, you can + also use the \ref format_http_time() and \ref parse_http_time() utilities. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpTime::BHttpTime() noexcept + \brief Constructs a new object and sets the timestamp to the current time. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpTime::BHttpTime(BDateTime date) + \brief Constructs a new object and sets the timestamp to \a date. + + \param date A valid \ref BDateTime object for the desired timestamp. + + \exception BHttpTime::InvalidInput The \a date does not contain a valid timestamp. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpTime::BHttpTime(const BString &dateString) + \brief Constructs a new object and parses the timestamp from \a dateString. + + \param dateString A string that contains a valid HTTP timestamp. The \a dateString must not + contain any characters, other than the timestamp. It is up to the caller to sanitize any + input, including trimming whitespace at the beginning and end of the string. + + \exception BHttpTime::InvalidInput The \a dateString cannot be parsed as a valid timestamp. + + \since Haiku R1 +*/ + + +/*! + \fn void BHttpTime::SetTo(BDateTime date) + \brief Set the current timestamp to \a date. + + \param date A valid \ref BDateTime object for the desired timestamp. + + \exception BHttpTime::InvalidInput The \a date does not contain a valid timestamp. + + \since Haiku R1 +*/ + + +/*! + \fn void BPrivate::Network::BHttpTime::SetTo(const BString &string) + \brief Set the current timestamp by parsing \a string. + + \param string A string that contains a valid HTTP timestamp. The \a dateString must not + contain any characters, other than the timestamp. It is up to the caller to sanitize any + input, including trimming whitespace at the beginning and end of the string. + + \exception BHttpTime::InvalidInput The \a dateString cannot be parsed as a valid timestamp. + + \since Haiku R1 +*/ + + +/*! + \fn BDateTime BPrivate::Network::BHttpTime::DateTime() const noexcept + \brief Get the current timestamp. + + \return A valid \ref BDateTime object that contains the timestamp that this object is currently + set to. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpTimeFormat BHttpTime::DateTimeFormat() const noexcept + \brief Get the format that the current timestamp parsed from. + + If the timestamp was parsed from a string, this method supplies a bit of information about what + format the original string was in. Note that for both the RFC 1132 and RFC 850 formats, the + parsing is slightly less strict than the RFC prescribes. This may mean that if you parse a + non-canonical string, and then format it back using the same format specifier, the two strings + may differ in content. + + If the timestamp was set by setting it to a \ref BDateTime object, then this will always return + \c BHttpTimeFormat::RFC1123. + + \return The \ref BHttpTimeFormat that describes the format the input string was in, or + \c BHttpTimeFormat::RFC1123 if the timestamp was set by a \ref BDateTime. + + \since Haiku R1 +*/ + + +/*! + \fn BString BHttpTime::ToString(BHttpTimeFormat outputFormat=BHttpTimeFormat::RFC1123) const + \brief Formats the timestamp to a string. + + \param outputFormat The requested outputformat. The default is the recommended RFC 1123 format. + + \return A string that contains the formatted timestamp. + + \exception std::bad_alloc In the future this method may throw this exception when the memory + for the output string cannot be allocated. + + \since Haiku R1 +*/ + + +/*! + \fn BString format_http_time(BDateTime timestamp, + BHttpTimeFormat outputFormat=BHttpTimeFormat::RFC1123) + \brief Format the \a timestamp into a string according to the \a outputFormat. + + See the description of the module in \ref HttpTime.h for more information about HTTP + timestamps. + + \param timestamp A valid \ref BDateTime object for the desired timestamp. + \param outputFormat The requested outputformat. The default is the recommended RFC 1123 format. + + \return A string that contains the formatted timestamp. + + \exception BHttpTime::InvalidInput The \a date does not contain a valid timestamp. + \exception std::bad_alloc In the future this method may throw this exception when the memory + for the output string cannot be allocated. + + \see \ref BHttpTime \ref parse_http_time() + + \since Haiku R1 +*/ + + +/*! + \fn BDateTime parse_http_time(const BString &string) + \brief Parse a \a string that contains a timestamp and return a \ref BDateTime object. + + See the description of the module in \ref HttpTime.h for more information about HTTP + timestamps. + + \param string A string that contains a valid HTTP timestamp. The \a dateString must not + contain any characters, other than the timestamp. It is up to the caller to sanitize any + input, including trimming whitespace at the beginning and end of the string. + + \exception BHttpTime::InvalidInput The \a string cannot be parsed as a valid timestamp. + + \see \ref BHttpTime \ref format_http_time() + + \since Haiku R1 +*/ + + +} // namespace BPrivate + +} // namespace Network + +#endif diff --git a/headers/private/netservices2/HttpTime.h b/headers/private/netservices2/HttpTime.h new file mode 100644 index 0000000000..3df2502885 --- /dev/null +++ b/headers/private/netservices2/HttpTime.h @@ -0,0 +1,71 @@ +/* + * Copyright 2010-2022 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + */ +#ifndef _B_HTTP_TIME_H_ +#define _B_HTTP_TIME_H_ + +#include + +#include +#include +#include + +namespace BPrivate { + +namespace Network { + +enum class BHttpTimeFormat : int8 { + RFC1123 = 0, + RFC850, + AscTime +}; + + +class BHttpTime { +public: + // Error type + class InvalidInput : public BError { + public: + InvalidInput(const char* origin, BString input); + + virtual const char* Message() const noexcept override; + virtual BString DebugMessage() const override; + + BString input; + }; + + // Constructors + BHttpTime() noexcept; + BHttpTime(BDateTime date); + BHttpTime(const BString& dateString); + + // Date modification + void SetTo(const BString& string); + void SetTo(BDateTime date); + + + // Date Access + BDateTime DateTime() const noexcept; + BHttpTimeFormat DateTimeFormat() const noexcept; + BString ToString(BHttpTimeFormat outputFormat = BHttpTimeFormat::RFC1123) const; + +private: + void _Parse(const BString& dateString); + + BDateTime fDate; + BHttpTimeFormat fDateFormat; +}; + + +// Convenience functions +BDateTime parse_http_time(const BString& string); +BString format_http_time(BDateTime timestamp, + BHttpTimeFormat outputFormat = BHttpTimeFormat::RFC1123); + + +} // namespace Network + +} // namespace BPrivate + +#endif // _B_HTTP_TIME_H_ diff --git a/src/kits/network/libnetservices2/HttpTime.cpp b/src/kits/network/libnetservices2/HttpTime.cpp new file mode 100644 index 0000000000..f2aecf7852 --- /dev/null +++ b/src/kits/network/libnetservices2/HttpTime.cpp @@ -0,0 +1,235 @@ +/* + * Copyright 2010-2022 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Christophe Huriaux, c.huriaux@gmail.com + * Adrien Destugues, pulkomandy@gmail.com + * Niels Sascha Reedijk, niels.reedijk@gmail.com + */ + +#include + +#include +#include + +#include + +using namespace BPrivate::Network; + + +// The formats used should be, in order of preference (according to RFC2616, +// section 3.3): +// RFC1123 / RFC822: "Sun, 06 Nov 1994 08:49:37 GMT" +// RFC850 : "Sunday, 06-Nov-94 08:49:37 GMT" (obsoleted by RFC 1036) +// asctime : "Sun Nov 6 08:49:37 1994" +// +// RFC1123 is the preferred one because it has 4 digit years. +// +// But of course in real life, all possible mixes of the formats are used. +// Believe it or not, it's even possible to find some website that gets this +// right and use one of the 3 formats above. +// Often seen variants are: +// - RFC1036 but with 4 digit year, +// - Missing or different timezone indicator +// - Invalid weekday +static const std::list> kDateFormats = { + // RFC822 + {BHttpTimeFormat::RFC1123, "%a, %d %b %Y %H:%M:%S GMT"},// canonical + {BHttpTimeFormat::RFC1123, "%a, %d %b %Y %H:%M:%S"}, // without timezone + // Standard RFC850 + {BHttpTimeFormat::RFC850, "%A, %d-%b-%y %H:%M:%S GMT"}, // canonical + {BHttpTimeFormat::RFC850, "%A, %d-%b-%y %H:%M:%S"}, // without timezone + // RFC 850 with 4 digit year + {BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S"}, // without timezone + {BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S GMT"}, // with 4-digit year + {BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S UTC"}, // "UTC" timezone + // asctime + {BHttpTimeFormat::AscTime, "%a %b %e %H:%M:%S %Y"}, +}; + + +// #pragma mark BHttpTime::InvalidInput + + +BHttpTime::InvalidInput::InvalidInput(const char* origin, BString input) + : + BError(origin), + input(std::move(input)) +{ + +} + + +const char* +BHttpTime::InvalidInput::Message() const noexcept +{ + if (input.IsEmpty()) + return "A HTTP timestamp cannot be empty"; + else + return "The HTTP timestamp string does not match the expected format"; +} + + +BString +BHttpTime::InvalidInput::DebugMessage() const +{ + BString output = BError::DebugMessage(); + if (!input.IsEmpty()) + output << ":\t " << input << "\n"; + return output; +} + + +// #pragma mark BHttpTime + + +BHttpTime::BHttpTime() noexcept + : + fDate(BDateTime::CurrentDateTime(B_GMT_TIME)), + fDateFormat(BHttpTimeFormat::RFC1123) +{ +} + + +BHttpTime::BHttpTime(BDateTime date) + : + fDate(date), + fDateFormat(BHttpTimeFormat::RFC1123) +{ + if (!fDate.IsValid()) + throw InvalidInput(__PRETTY_FUNCTION__, "Invalid BDateTime object"); +} + + +BHttpTime::BHttpTime(const BString& dateString) + : + fDate(0), + fDateFormat(BHttpTimeFormat::RFC1123) +{ + _Parse(dateString); +} + + +// #pragma mark Date modification + + +void +BHttpTime::SetTo(const BString& string) +{ + _Parse(string); +} + + +void +BHttpTime::SetTo(BDateTime date) +{ + if (!date.IsValid()) + throw InvalidInput(__PRETTY_FUNCTION__, "Invalid BDateTime object"); + + fDate = date; + fDateFormat = BHttpTimeFormat::RFC1123; +} + + +// #pragma mark Date Access + + +BDateTime +BHttpTime::DateTime() const noexcept +{ + return fDate; +} + + +BHttpTimeFormat +BHttpTime::DateTimeFormat() const noexcept +{ + return fDateFormat; +} + + +BString +BHttpTime::ToString(BHttpTimeFormat outputFormat) const +{ + BString expirationFinal; + struct tm expirationTm = {}; + expirationTm.tm_sec = fDate.Time().Second(); + expirationTm.tm_min = fDate.Time().Minute(); + expirationTm.tm_hour = fDate.Time().Hour(); + expirationTm.tm_mday = fDate.Date().Day(); + expirationTm.tm_mon = fDate.Date().Month() - 1; + expirationTm.tm_year = fDate.Date().Year() - 1900; + // strftime starts weekday count at 0 for Sunday, + // while DayOfWeek starts at 1 for Monday and thus uses 7 for Sunday + expirationTm.tm_wday = fDate.Date().DayOfWeek() % 7; + expirationTm.tm_yday = 0; + expirationTm.tm_isdst = 0; + + for (auto& [format, formatString]: kDateFormats) { + if (format != outputFormat) + continue; + + static const uint16 kTimetToStringMaxLength = 128; + char expirationString[kTimetToStringMaxLength + 1]; + size_t strLength; + + strLength = strftime(expirationString, kTimetToStringMaxLength, formatString, + &expirationTm); + + expirationFinal.SetTo(expirationString, strLength); + break; + } + + return expirationFinal; +} + + +void +BHttpTime::_Parse(const BString& dateString) +{ + if (dateString.Length() < 4) + throw InvalidInput(__PRETTY_FUNCTION__, dateString); + + struct tm expireTime = {}; + + bool found = false; + for (auto& [format, formatString]: kDateFormats) { + const char* result = strptime(dateString.String(), formatString, &expireTime); + + if (result == dateString.String() + dateString.Length()) { + fDateFormat = format; + found = true; + break; + } + } + + // Did we identify some valid format? + if (!found) + throw InvalidInput(__PRETTY_FUNCTION__, dateString); + + // Now convert the struct tm from strptime into a BDateTime. + BTime time(expireTime.tm_hour, expireTime.tm_min, expireTime.tm_sec); + BDate date(expireTime.tm_year + 1900, expireTime.tm_mon + 1, + expireTime.tm_mday); + fDate = BDateTime(date, time); +} + + +// #pragma mark Convenience Functions + + +BDateTime +BPrivate::Network::parse_http_time(const BString& string) +{ + BHttpTime httpTime(string); + return httpTime.DateTime(); +} + + +BString +BPrivate::Network::format_http_time(BDateTime timestamp, BHttpTimeFormat outputFormat) +{ + BHttpTime httpTime(timestamp); + return httpTime.ToString(outputFormat); +} diff --git a/src/kits/network/libnetservices2/Jamfile b/src/kits/network/libnetservices2/Jamfile index c5a5cf00b7..bae7426e14 100644 --- a/src/kits/network/libnetservices2/Jamfile +++ b/src/kits/network/libnetservices2/Jamfile @@ -24,6 +24,7 @@ for architectureObject in [ MultiArchSubDirSetup ] { HttpResult.cpp HttpSession.cpp HttpStream.cpp + HttpTime.cpp NetServicesMisc.cpp ; diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp index 47e3cfac0e..24412b7845 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp @@ -13,20 +13,27 @@ #include #include +#include #include #include #include #include +#include #include #include +using BPrivate::BDateTime; using BPrivate::Network::BHttpFields; using BPrivate::Network::BHttpMethod; using BPrivate::Network::BHttpRequest; +using BPrivate::Network::BHttpRequestStream; using BPrivate::Network::BHttpResult; using BPrivate::Network::BHttpSession; -using BPrivate::Network::BHttpRequestStream; +using BPrivate::Network::BHttpTime; +using BPrivate::Network::BHttpTimeFormat; using BPrivate::Network::BNetworkRequestError; +using BPrivate::Network::format_http_time; +using BPrivate::Network::parse_http_time; using namespace std::literals; @@ -360,6 +367,52 @@ HttpProtocolTest::HttpRequestStreamTest() } +void +HttpProtocolTest::HttpTimeTest() +{ + const std::vector kValidTimeStrings = { + "Sun, 07 Dec 2003 16:01:00 GMT", + "Sun, 07 Dec 2003 16:01:00", + "Sunday, 07-Dec-03 16:01:00 GMT", + "Sunday, 07-Dec-03 16:01:00 GMT", + "Sunday, 07-Dec-2003 16:01:00", + "Sunday, 07-Dec-2003 16:01:00 GMT", + "Sunday, 07-Dec-2003 16:01:00 UTC", + "Sun Dec 7 16:01:00 2003" + }; + const BDateTime kExpectedDateTime = {BDate{2003, 12, 7}, BTime{16, 01, 0}}; + + for (const auto& timeString: kValidTimeStrings) { + CPPUNIT_ASSERT(kExpectedDateTime == parse_http_time(timeString)); + } + + const std::vector kInvalidTimeStrings = { + "Sun, 07 Dec 2003", // Date only + "Sun, 07 Dec 2003 16:01:00 BST", // Invalid timezone + "On Sun, 07 Dec 2003 16:01:00 GMT", // Extra data in front of the string + }; + + for (const auto& timeString: kInvalidTimeStrings) { + try { + parse_http_time(timeString); + BString errorMessage = "Expected exception with invalid timestring: "; + errorMessage.Append(timeString); + CPPUNIT_FAIL(errorMessage.String()); + } catch (const BHttpTime::InvalidInput& e) { + // expected exception; continue + } + } + + // Validate format_http_time() + CPPUNIT_ASSERT_EQUAL(BString("Sun, 07 Dec 2003 16:01:00 GMT"), + format_http_time(kExpectedDateTime)); + CPPUNIT_ASSERT_EQUAL(BString("Sunday, 07-Dec-03 16:01:00 GMT"), + format_http_time(kExpectedDateTime, BHttpTimeFormat::RFC850)); + CPPUNIT_ASSERT_EQUAL(BString("Sun Dec 7 16:01:00 2003"), + format_http_time(kExpectedDateTime, BHttpTimeFormat::AscTime)); +} + + /* static */ void HttpProtocolTest::AddTests(BTestSuite& parent) { @@ -373,6 +426,8 @@ HttpProtocolTest::AddTests(BTestSuite& parent) "HttpProtocolTest::HttpRequestTest", &HttpProtocolTest::HttpRequestTest)); suite.addTest(new CppUnit::TestCaller( "HttpProtocolTest::HttpRequestStreamTest", &HttpProtocolTest::HttpRequestStreamTest)); + suite.addTest(new CppUnit::TestCaller( + "HttpProtocolTest::HttpTimeTest", &HttpProtocolTest::HttpTimeTest)); parent.addTest("HttpProtocolTest", &suite); } diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.h b/src/tests/kits/net/netservices2/HttpProtocolTest.h index fa42c0f8be..2a4872f5e1 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.h +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.h @@ -24,6 +24,7 @@ public: void HttpMethodTest(); void HttpRequestTest(); void HttpRequestStreamTest(); + void HttpTimeTest(); static void AddTests(BTestSuite& suite); };