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
This commit is contained in:
Niels Sascha Reedijk
2022-05-08 06:59:51 +01:00
parent 13bfff7be3
commit cf54474e38
6 changed files with 623 additions and 1 deletions
+259
View File
@@ -0,0 +1,259 @@
/*
* Copyright 2022 Haiku, Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Niels Sascha Reedijk, [email protected]
*
* 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:
<table>
<tr><td>RFC1123 / RFC822</td><td>Sun, 06 Nov 1994 08:49:37 GMT</td></tr>
<tr><td>RFC850</td><td>Sunday, 06-Nov-94 08:49:37 GMT</td></tr>
<tr><td>asctime</td><td>Sun Nov 6 08:49:37 1994</td></tr>
</table>
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
+71
View File
@@ -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 <ctime>
#include <DateTime.h>
#include <ErrorsExt.h>
#include <String.h>
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_
@@ -0,0 +1,235 @@
/*
* Copyright 2010-2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Christophe Huriaux, [email protected]
* Adrien Destugues, [email protected]
* Niels Sascha Reedijk, [email protected]
*/
#include <HttpTime.h>
#include <list>
#include <new>
#include <cstdio>
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<std::pair<BHttpTimeFormat, const char*>> 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);
}
+1
View File
@@ -24,6 +24,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
HttpResult.cpp HttpResult.cpp
HttpSession.cpp HttpSession.cpp
HttpStream.cpp HttpStream.cpp
HttpTime.cpp
NetServicesMisc.cpp NetServicesMisc.cpp
; ;
@@ -13,20 +13,27 @@
#include <cppunit/TestSuite.h> #include <cppunit/TestSuite.h>
#include <tools/cppunit/ThreadedTestCaller.h> #include <tools/cppunit/ThreadedTestCaller.h>
#include <DateTime.h>
#include <HttpFields.h> #include <HttpFields.h>
#include <HttpRequest.h> #include <HttpRequest.h>
#include <HttpResult.h> #include <HttpResult.h>
#include <HttpStream.h> #include <HttpStream.h>
#include <HttpTime.h>
#include <NetServicesDefs.h> #include <NetServicesDefs.h>
#include <Url.h> #include <Url.h>
using BPrivate::BDateTime;
using BPrivate::Network::BHttpFields; using BPrivate::Network::BHttpFields;
using BPrivate::Network::BHttpMethod; using BPrivate::Network::BHttpMethod;
using BPrivate::Network::BHttpRequest; using BPrivate::Network::BHttpRequest;
using BPrivate::Network::BHttpRequestStream;
using BPrivate::Network::BHttpResult; using BPrivate::Network::BHttpResult;
using BPrivate::Network::BHttpSession; using BPrivate::Network::BHttpSession;
using BPrivate::Network::BHttpRequestStream; using BPrivate::Network::BHttpTime;
using BPrivate::Network::BHttpTimeFormat;
using BPrivate::Network::BNetworkRequestError; using BPrivate::Network::BNetworkRequestError;
using BPrivate::Network::format_http_time;
using BPrivate::Network::parse_http_time;
using namespace std::literals; using namespace std::literals;
@@ -360,6 +367,52 @@ HttpProtocolTest::HttpRequestStreamTest()
} }
void
HttpProtocolTest::HttpTimeTest()
{
const std::vector<BString> 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<BString> 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 /* static */ void
HttpProtocolTest::AddTests(BTestSuite& parent) HttpProtocolTest::AddTests(BTestSuite& parent)
{ {
@@ -373,6 +426,8 @@ HttpProtocolTest::AddTests(BTestSuite& parent)
"HttpProtocolTest::HttpRequestTest", &HttpProtocolTest::HttpRequestTest)); "HttpProtocolTest::HttpRequestTest", &HttpProtocolTest::HttpRequestTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>( suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpRequestStreamTest", &HttpProtocolTest::HttpRequestStreamTest)); "HttpProtocolTest::HttpRequestStreamTest", &HttpProtocolTest::HttpRequestStreamTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpTimeTest", &HttpProtocolTest::HttpTimeTest));
parent.addTest("HttpProtocolTest", &suite); parent.addTest("HttpProtocolTest", &suite);
} }
@@ -24,6 +24,7 @@ public:
void HttpMethodTest(); void HttpMethodTest();
void HttpRequestTest(); void HttpRequestTest();
void HttpRequestStreamTest(); void HttpRequestStreamTest();
void HttpTimeTest();
static void AddTests(BTestSuite& suite); static void AddTests(BTestSuite& suite);
}; };