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 / RFC822 | Sun, 06 Nov 1994 08:49:37 GMT |
+ | RFC850 | Sunday, 06-Nov-94 08:49:37 GMT |
+ | asctime | Sun 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);
};