1/*
2 * Copyright 2010-2022 Haiku Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Christophe Huriaux, c.huriaux@gmail.com
7 *		Adrien Destugues, pulkomandy@gmail.com
8 *		Niels Sascha Reedijk, niels.reedijk@gmail.com
9 */
10
11#include <HttpTime.h>
12
13#include <list>
14#include <new>
15
16#include <cstdio>
17
18using namespace BPrivate::Network;
19
20
21// The formats used should be, in order of preference (according to RFC2616,
22// section 3.3):
23// RFC1123 / RFC822: "Sun, 06 Nov 1994 08:49:37 GMT"
24// RFC850          : "Sunday, 06-Nov-94 08:49:37 GMT" (obsoleted by RFC 1036)
25// asctime         : "Sun Nov 6 08:49:37 1994"
26//
27// RFC1123 is the preferred one because it has 4 digit years.
28//
29// But of course in real life, all possible mixes of the formats are used.
30// Believe it or not, it's even possible to find some website that gets this
31// right and use one of the 3 formats above.
32// Often seen variants are:
33// - RFC1036 but with 4 digit year,
34// - Missing or different timezone indicator
35// - Invalid weekday
36static const std::list<std::pair<BHttpTimeFormat, const char*>> kDateFormats = {
37	// RFC822
38	{BHttpTimeFormat::RFC1123, "%a, %d %b %Y %H:%M:%S GMT"}, // canonical
39	{BHttpTimeFormat::RFC1123, "%a, %d %b %Y %H:%M:%S"}, // without timezone
40	// Standard RFC850
41	{BHttpTimeFormat::RFC850, "%A, %d-%b-%y %H:%M:%S GMT"}, // canonical
42	{BHttpTimeFormat::RFC850, "%A, %d-%b-%y %H:%M:%S"}, // without timezone
43	// RFC 850 with 4 digit year
44	{BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S"}, // without timezone
45	{BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S GMT"}, // with 4-digit year
46	{BHttpTimeFormat::RFC850, "%a, %d-%b-%Y %H:%M:%S UTC"}, // "UTC" timezone
47	// asctime
48	{BHttpTimeFormat::AscTime, "%a %b %e %H:%M:%S %Y"},
49};
50
51
52// #pragma mark BHttpTime::InvalidInput
53
54
55BHttpTime::InvalidInput::InvalidInput(const char* origin, BString input)
56	:
57	BError(origin),
58	input(std::move(input))
59{
60}
61
62
63const char*
64BHttpTime::InvalidInput::Message() const noexcept
65{
66	if (input.IsEmpty())
67		return "A HTTP timestamp cannot be empty";
68	else
69		return "The HTTP timestamp string does not match the expected format";
70}
71
72
73BString
74BHttpTime::InvalidInput::DebugMessage() const
75{
76	BString output = BError::DebugMessage();
77	if (!input.IsEmpty())
78		output << ":\t " << input << "\n";
79	return output;
80}
81
82
83// #pragma mark BHttpTime
84
85
86BHttpTime::BHttpTime() noexcept
87	:
88	fDate(BDateTime::CurrentDateTime(B_GMT_TIME)),
89	fDateFormat(BHttpTimeFormat::RFC1123)
90{
91}
92
93
94BHttpTime::BHttpTime(BDateTime date)
95	:
96	fDate(date),
97	fDateFormat(BHttpTimeFormat::RFC1123)
98{
99	if (!fDate.IsValid())
100		throw InvalidInput(__PRETTY_FUNCTION__, "Invalid BDateTime object");
101}
102
103
104BHttpTime::BHttpTime(const BString& dateString)
105	:
106	fDate(0),
107	fDateFormat(BHttpTimeFormat::RFC1123)
108{
109	_Parse(dateString);
110}
111
112
113// #pragma mark Date modification
114
115
116void
117BHttpTime::SetTo(const BString& string)
118{
119	_Parse(string);
120}
121
122
123void
124BHttpTime::SetTo(BDateTime date)
125{
126	if (!date.IsValid())
127		throw InvalidInput(__PRETTY_FUNCTION__, "Invalid BDateTime object");
128
129	fDate = date;
130	fDateFormat = BHttpTimeFormat::RFC1123;
131}
132
133
134// #pragma mark Date Access
135
136
137BDateTime
138BHttpTime::DateTime() const noexcept
139{
140	return fDate;
141}
142
143
144BHttpTimeFormat
145BHttpTime::DateTimeFormat() const noexcept
146{
147	return fDateFormat;
148}
149
150
151BString
152BHttpTime::ToString(BHttpTimeFormat outputFormat) const
153{
154	BString expirationFinal;
155	struct tm expirationTm = {};
156	expirationTm.tm_sec = fDate.Time().Second();
157	expirationTm.tm_min = fDate.Time().Minute();
158	expirationTm.tm_hour = fDate.Time().Hour();
159	expirationTm.tm_mday = fDate.Date().Day();
160	expirationTm.tm_mon = fDate.Date().Month() - 1;
161	expirationTm.tm_year = fDate.Date().Year() - 1900;
162	// strftime starts weekday count at 0 for Sunday,
163	// while DayOfWeek starts at 1 for Monday and thus uses 7 for Sunday
164	expirationTm.tm_wday = fDate.Date().DayOfWeek() % 7;
165	expirationTm.tm_yday = 0;
166	expirationTm.tm_isdst = 0;
167
168	for (auto& [format, formatString]: kDateFormats) {
169		if (format != outputFormat)
170			continue;
171
172		static const uint16 kTimetToStringMaxLength = 128;
173		char expirationString[kTimetToStringMaxLength + 1];
174		size_t strLength;
175
176		strLength
177			= strftime(expirationString, kTimetToStringMaxLength, formatString, &expirationTm);
178
179		expirationFinal.SetTo(expirationString, strLength);
180		break;
181	}
182
183	return expirationFinal;
184}
185
186
187void
188BHttpTime::_Parse(const BString& dateString)
189{
190	if (dateString.Length() < 4)
191		throw InvalidInput(__PRETTY_FUNCTION__, dateString);
192
193	struct tm expireTime = {};
194
195	bool found = false;
196	for (auto& [format, formatString]: kDateFormats) {
197		const char* result = strptime(dateString.String(), formatString, &expireTime);
198
199		if (result == dateString.String() + dateString.Length()) {
200			fDateFormat = format;
201			found = true;
202			break;
203		}
204	}
205
206	// Did we identify some valid format?
207	if (!found)
208		throw InvalidInput(__PRETTY_FUNCTION__, dateString);
209
210	// Now convert the struct tm from strptime into a BDateTime.
211	BTime time(expireTime.tm_hour, expireTime.tm_min, expireTime.tm_sec);
212	BDate date(expireTime.tm_year + 1900, expireTime.tm_mon + 1, expireTime.tm_mday);
213	fDate = BDateTime(date, time);
214}
215
216
217// #pragma mark Convenience Functions
218
219
220BDateTime
221BPrivate::Network::parse_http_time(const BString& string)
222{
223	BHttpTime httpTime(string);
224	return httpTime.DateTime();
225}
226
227
228BString
229BPrivate::Network::format_http_time(BDateTime timestamp, BHttpTimeFormat outputFormat)
230{
231	BHttpTime httpTime(timestamp);
232	return httpTime.ToString(outputFormat);
233}
234