1/*
2 * Copyright 2022 Haiku Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Niels Sascha Reedijk, niels.reedijk@gmail.com
7 */
8
9#include <HttpRequest.h>
10
11#include <algorithm>
12#include <ctype.h>
13#include <sstream>
14#include <utility>
15
16#include <DataIO.h>
17#include <HttpFields.h>
18#include <MimeType.h>
19#include <NetServicesDefs.h>
20#include <Url.h>
21
22#include "HttpBuffer.h"
23#include "HttpPrivate.h"
24
25using namespace std::literals;
26using namespace BPrivate::Network;
27
28
29// #pragma mark -- BHttpMethod::InvalidMethod
30
31
32BHttpMethod::InvalidMethod::InvalidMethod(const char* origin, BString input)
33	:
34	BError(origin),
35	input(std::move(input))
36{
37}
38
39
40const char*
41BHttpMethod::InvalidMethod::Message() const noexcept
42{
43	if (input.IsEmpty())
44		return "The HTTP method cannot be empty";
45	else
46		return "Unsupported characters in the HTTP method";
47}
48
49
50BString
51BHttpMethod::InvalidMethod::DebugMessage() const
52{
53	BString output = BError::DebugMessage();
54	if (!input.IsEmpty())
55		output << ":\t " << input << "\n";
56	return output;
57}
58
59
60// #pragma mark -- BHttpMethod
61
62
63BHttpMethod::BHttpMethod(Verb verb) noexcept
64	:
65	fMethod(verb)
66{
67}
68
69
70BHttpMethod::BHttpMethod(const std::string_view& verb)
71	:
72	fMethod(BString(verb.data(), verb.length()))
73{
74	if (verb.size() == 0 || !validate_http_token_string(verb))
75		throw BHttpMethod::InvalidMethod(
76			__PRETTY_FUNCTION__, std::move(std::get<BString>(fMethod)));
77}
78
79
80BHttpMethod::BHttpMethod(const BHttpMethod& other) = default;
81
82
83BHttpMethod::BHttpMethod(BHttpMethod&& other) noexcept
84	:
85	fMethod(std::move(other.fMethod))
86{
87	other.fMethod = Get;
88}
89
90
91BHttpMethod::~BHttpMethod() = default;
92
93
94BHttpMethod& BHttpMethod::operator=(const BHttpMethod& other) = default;
95
96
97BHttpMethod&
98BHttpMethod::operator=(BHttpMethod&& other) noexcept
99{
100	fMethod = std::move(other.fMethod);
101	other.fMethod = Get;
102	return *this;
103}
104
105
106bool
107BHttpMethod::operator==(const BHttpMethod::Verb& other) const noexcept
108{
109	if (std::holds_alternative<Verb>(fMethod)) {
110		return std::get<Verb>(fMethod) == other;
111	} else {
112		BHttpMethod otherMethod(other);
113		auto otherMethodSv = otherMethod.Method();
114		return std::get<BString>(fMethod).Compare(otherMethodSv.data(), otherMethodSv.size()) == 0;
115	}
116}
117
118
119bool
120BHttpMethod::operator!=(const BHttpMethod::Verb& other) const noexcept
121{
122	return !operator==(other);
123}
124
125
126const std::string_view
127BHttpMethod::Method() const noexcept
128{
129	if (std::holds_alternative<Verb>(fMethod)) {
130		switch (std::get<Verb>(fMethod)) {
131			case Get:
132				return "GET"sv;
133			case Head:
134				return "HEAD"sv;
135			case Post:
136				return "POST"sv;
137			case Put:
138				return "PUT"sv;
139			case Delete:
140				return "DELETE"sv;
141			case Connect:
142				return "CONNECT"sv;
143			case Options:
144				return "OPTIONS"sv;
145			case Trace:
146				return "TRACE"sv;
147			default:
148				// should never be reached
149				std::abort();
150		}
151	} else {
152		const auto& methodString = std::get<BString>(fMethod);
153		// the following constructor is not noexcept, but we know we pass in valid data
154		return std::string_view(methodString.String());
155	}
156}
157
158
159// #pragma mark -- BHttpRequest::Data
160static const BUrl kDefaultUrl = BUrl();
161static const BHttpMethod kDefaultMethod = BHttpMethod::Get;
162static const BHttpFields kDefaultOptionalFields = BHttpFields();
163
164struct BHttpRequest::Data {
165	BUrl url = kDefaultUrl;
166	BHttpMethod method = kDefaultMethod;
167	uint8 maxRedirections = 8;
168	BHttpFields optionalFields;
169	std::optional<BHttpAuthentication> authentication;
170	bool stopOnError = false;
171	bigtime_t timeout = B_INFINITE_TIMEOUT;
172	std::optional<Body> requestBody;
173};
174
175
176// #pragma mark -- BHttpRequest helper functions
177
178
179/*!
180	\brief Build basic authentication header
181*/
182static inline BString
183build_basic_http_header(const BString& username, const BString& password)
184{
185	BString basicEncode, result;
186	basicEncode << username << ":" << password;
187	result << "Basic " << encode_to_base64(basicEncode);
188	return result;
189}
190
191
192// #pragma mark -- BHttpRequest
193
194
195BHttpRequest::BHttpRequest()
196	:
197	fData(std::make_unique<Data>())
198{
199}
200
201
202BHttpRequest::BHttpRequest(const BUrl& url)
203	:
204	fData(std::make_unique<Data>())
205{
206	SetUrl(url);
207}
208
209
210BHttpRequest::BHttpRequest(BHttpRequest&& other) noexcept = default;
211
212
213BHttpRequest::~BHttpRequest() = default;
214
215
216BHttpRequest& BHttpRequest::operator=(BHttpRequest&&) noexcept = default;
217
218
219bool
220BHttpRequest::IsEmpty() const noexcept
221{
222	return (!fData || !fData->url.IsValid());
223}
224
225
226const BHttpAuthentication*
227BHttpRequest::Authentication() const noexcept
228{
229	if (fData && fData->authentication)
230		return std::addressof(*fData->authentication);
231	return nullptr;
232}
233
234
235const BHttpFields&
236BHttpRequest::Fields() const noexcept
237{
238	if (!fData)
239		return kDefaultOptionalFields;
240	return fData->optionalFields;
241}
242
243
244uint8
245BHttpRequest::MaxRedirections() const noexcept
246{
247	if (!fData)
248		return 8;
249	return fData->maxRedirections;
250}
251
252
253const BHttpMethod&
254BHttpRequest::Method() const noexcept
255{
256	if (!fData)
257		return kDefaultMethod;
258	return fData->method;
259}
260
261
262const BHttpRequest::Body*
263BHttpRequest::RequestBody() const noexcept
264{
265	if (fData && fData->requestBody)
266		return std::addressof(*fData->requestBody);
267	return nullptr;
268}
269
270
271bool
272BHttpRequest::StopOnError() const noexcept
273{
274	if (!fData)
275		return false;
276	return fData->stopOnError;
277}
278
279
280bigtime_t
281BHttpRequest::Timeout() const noexcept
282{
283	if (!fData)
284		return B_INFINITE_TIMEOUT;
285	return fData->timeout;
286}
287
288
289const BUrl&
290BHttpRequest::Url() const noexcept
291{
292	if (!fData)
293		return kDefaultUrl;
294	return fData->url;
295}
296
297
298void
299BHttpRequest::SetAuthentication(const BHttpAuthentication& authentication)
300{
301	if (!fData)
302		fData = std::make_unique<Data>();
303
304	fData->authentication = authentication;
305}
306
307
308static constexpr std::array<std::string_view, 6> fReservedOptionalFieldNames
309	= {"Host"sv, "Accept-Encoding"sv, "Connection"sv, "Content-Type"sv, "Content-Length"sv};
310
311
312void
313BHttpRequest::SetFields(const BHttpFields& fields)
314{
315	if (!fData)
316		fData = std::make_unique<Data>();
317
318	for (auto& field: fields) {
319		if (std::find(fReservedOptionalFieldNames.begin(), fReservedOptionalFieldNames.end(),
320				field.Name())
321			!= fReservedOptionalFieldNames.end()) {
322			std::string_view fieldName = field.Name();
323			throw BHttpFields::InvalidInput(
324				__PRETTY_FUNCTION__, BString(fieldName.data(), fieldName.size()));
325		}
326	}
327	fData->optionalFields = fields;
328}
329
330
331void
332BHttpRequest::SetMaxRedirections(uint8 maxRedirections)
333{
334	if (!fData)
335		fData = std::make_unique<Data>();
336	fData->maxRedirections = maxRedirections;
337}
338
339
340void
341BHttpRequest::SetMethod(const BHttpMethod& method)
342{
343	if (!fData)
344		fData = std::make_unique<Data>();
345	fData->method = method;
346}
347
348
349void
350BHttpRequest::SetRequestBody(
351	std::unique_ptr<BDataIO> input, BString mimeType, std::optional<off_t> size)
352{
353	if (input == nullptr)
354		throw std::invalid_argument("input cannot be null");
355
356	// TODO: support optional mimetype arguments like type/subtype;parameter=value
357	if (!BMimeType::IsValid(mimeType.String()))
358		throw std::invalid_argument("mimeType must be a valid mimetype");
359
360	// TODO: review if there should be complex validation between the method and whether or not
361	// there is a request body. The current implementation does the validation at the request
362	// generation stage, where GET, HEAD, OPTIONS, CONNECT and TRACE will not submit a body.
363
364	if (!fData)
365		fData = std::make_unique<Data>();
366	fData->requestBody = {std::move(input), std::move(mimeType), size};
367
368	// Check if the input is a BPositionIO, and if so, store the current position, so that it can
369	// be rewinded in case of a redirect.
370	auto inputPositionIO = dynamic_cast<BPositionIO*>(fData->requestBody->input.get());
371	if (inputPositionIO != nullptr)
372		fData->requestBody->startPosition = inputPositionIO->Position();
373}
374
375
376void
377BHttpRequest::SetStopOnError(bool stopOnError)
378{
379	if (!fData)
380		fData = std::make_unique<Data>();
381	fData->stopOnError = stopOnError;
382}
383
384
385void
386BHttpRequest::SetTimeout(bigtime_t timeout)
387{
388	if (!fData)
389		fData = std::make_unique<Data>();
390	fData->timeout = timeout;
391}
392
393
394void
395BHttpRequest::SetUrl(const BUrl& url)
396{
397	if (!fData)
398		fData = std::make_unique<Data>();
399
400	if (!url.IsValid())
401		throw BInvalidUrl(__PRETTY_FUNCTION__, BUrl(url));
402	if (url.Protocol() != "http" && url.Protocol() != "https") {
403		// TODO: optimize BStringList with modern language features
404		BStringList list;
405		list.Add("http");
406		list.Add("https");
407		throw BUnsupportedProtocol(__PRETTY_FUNCTION__, BUrl(url), list);
408	}
409	fData->url = url;
410}
411
412
413void
414BHttpRequest::ClearAuthentication() noexcept
415{
416	if (fData)
417		fData->authentication = std::nullopt;
418}
419
420
421std::unique_ptr<BDataIO>
422BHttpRequest::ClearRequestBody() noexcept
423{
424	if (fData && fData->requestBody) {
425		auto body = std::move(fData->requestBody->input);
426		fData->requestBody = std::nullopt;
427		return body;
428	}
429	return nullptr;
430}
431
432
433BString
434BHttpRequest::HeaderToString() const
435{
436	HttpBuffer buffer;
437	SerializeHeaderTo(buffer);
438
439	return BString(static_cast<const char*>(buffer.Data().data()), buffer.RemainingBytes());
440}
441
442
443/*!
444	\brief Private method used by BHttpSession::Request to rewind the content in case of redirect
445
446	\retval true Content was rewinded successfully. Also the case if there is no content
447	\retval false Cannot/could not rewind content.
448*/
449bool
450BHttpRequest::RewindBody() noexcept
451{
452	if (fData && fData->requestBody && fData->requestBody->startPosition) {
453		auto inputData = dynamic_cast<BPositionIO*>(fData->requestBody->input.get());
454		return *fData->requestBody->startPosition
455			== inputData->Seek(*fData->requestBody->startPosition, SEEK_SET);
456	}
457	return true;
458}
459
460
461/*!
462	\brief Private method used by HttpSerializer::SetTo() to serialize the header data into a
463		buffer.
464*/
465void
466BHttpRequest::SerializeHeaderTo(HttpBuffer& buffer) const
467{
468	// Method & URL
469	//	TODO: proxy
470	buffer << fData->method.Method() << " "sv;
471	if (fData->url.HasPath() && fData->url.Path().Length() > 0)
472		buffer << std::string_view(fData->url.Path().String());
473	else
474		buffer << "/"sv;
475
476	if (fData->url.HasRequest())
477		buffer << "?"sv << Url().Request().String();
478
479	// TODO: switch between HTTP 1.0 and 1.1 based on configuration
480	buffer << " HTTP/1.1\r\n"sv;
481
482	BHttpFields outputFields;
483	if (true /* http == 1.1 */) {
484		BString host = fData->url.Host();
485		int defaultPort = fData->url.Protocol() == "http" ? 80 : 443;
486		if (fData->url.HasPort() && fData->url.Port() != defaultPort)
487			host << ':' << fData->url.Port();
488
489		outputFields.AddFields({
490			{"Host"sv, std::string_view(host.String())}, {"Accept-Encoding"sv, "gzip"sv},
491			// Allows the server to compress data using the "gzip" format.
492			// "deflate" is not supported, because there are two interpretations
493			// of what it means (the RFC and Microsoft products), and we don't
494			// want to handle this. Very few websites support only deflate,
495			// and most of them will send gzip, or at worst, uncompressed data.
496			{"Connection"sv, "close"sv}
497			// Let the remote server close the connection after response since
498			// we don't handle multiple request on a single connection
499		});
500	}
501
502	if (fData->authentication) {
503		// This request will add a Basic authorization header
504		BString authorization = build_basic_http_header(
505			fData->authentication->username, fData->authentication->password);
506		outputFields.AddField("Authorization"sv, std::string_view(authorization.String()));
507	}
508
509	if (fData->requestBody) {
510		outputFields.AddField(
511			"Content-Type"sv, std::string_view(fData->requestBody->mimeType.String()));
512		if (fData->requestBody->size)
513			outputFields.AddField("Content-Length"sv, std::to_string(*fData->requestBody->size));
514		else
515			throw BRuntimeError(__PRETTY_FUNCTION__,
516				"Transfer body with unknown content length; chunked transfer not supported");
517	}
518
519	for (const auto& field: outputFields)
520		buffer << field.RawField() << "\r\n"sv;
521
522	for (const auto& field: fData->optionalFields)
523		buffer << field.RawField() << "\r\n"sv;
524
525	buffer << "\r\n"sv;
526}
527