xref: /haiku/src/kits/network/libnetservices2/HttpRequest.cpp (revision 52c4471a3024d2eb81fe88e2c3982b9f8daa5e56)
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 
25 using namespace std::literals;
26 using namespace BPrivate::Network;
27 
28 
29 // #pragma mark -- BHttpMethod::InvalidMethod
30 
31 
32 BHttpMethod::InvalidMethod::InvalidMethod(const char* origin, BString input)
33 	:
34 	BError(origin),
35 	input(std::move(input))
36 {
37 }
38 
39 
40 const char*
41 BHttpMethod::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 
50 BString
51 BHttpMethod::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 
63 BHttpMethod::BHttpMethod(Verb verb) noexcept
64 	:
65 	fMethod(verb)
66 {
67 }
68 
69 
70 BHttpMethod::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 
80 BHttpMethod::BHttpMethod(const BHttpMethod& other) = default;
81 
82 
83 BHttpMethod::BHttpMethod(BHttpMethod&& other) noexcept
84 	:
85 	fMethod(std::move(other.fMethod))
86 {
87 	other.fMethod = Get;
88 }
89 
90 
91 BHttpMethod::~BHttpMethod() = default;
92 
93 
94 BHttpMethod& BHttpMethod::operator=(const BHttpMethod& other) = default;
95 
96 
97 BHttpMethod&
98 BHttpMethod::operator=(BHttpMethod&& other) noexcept
99 {
100 	fMethod = std::move(other.fMethod);
101 	other.fMethod = Get;
102 	return *this;
103 }
104 
105 
106 bool
107 BHttpMethod::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 
119 bool
120 BHttpMethod::operator!=(const BHttpMethod::Verb& other) const noexcept
121 {
122 	return !operator==(other);
123 }
124 
125 
126 const std::string_view
127 BHttpMethod::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
160 static const BUrl kDefaultUrl = BUrl();
161 static const BHttpMethod kDefaultMethod = BHttpMethod::Get;
162 static const BHttpFields kDefaultOptionalFields = BHttpFields();
163 
164 struct 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 */
182 static inline BString
183 build_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 
195 BHttpRequest::BHttpRequest()
196 	:
197 	fData(std::make_unique<Data>())
198 {
199 }
200 
201 
202 BHttpRequest::BHttpRequest(const BUrl& url)
203 	:
204 	fData(std::make_unique<Data>())
205 {
206 	SetUrl(url);
207 }
208 
209 
210 BHttpRequest::BHttpRequest(BHttpRequest&& other) noexcept = default;
211 
212 
213 BHttpRequest::~BHttpRequest() = default;
214 
215 
216 BHttpRequest& BHttpRequest::operator=(BHttpRequest&&) noexcept = default;
217 
218 
219 bool
220 BHttpRequest::IsEmpty() const noexcept
221 {
222 	return (!fData || !fData->url.IsValid());
223 }
224 
225 
226 const BHttpAuthentication*
227 BHttpRequest::Authentication() const noexcept
228 {
229 	if (fData && fData->authentication)
230 		return std::addressof(*fData->authentication);
231 	return nullptr;
232 }
233 
234 
235 const BHttpFields&
236 BHttpRequest::Fields() const noexcept
237 {
238 	if (!fData)
239 		return kDefaultOptionalFields;
240 	return fData->optionalFields;
241 }
242 
243 
244 uint8
245 BHttpRequest::MaxRedirections() const noexcept
246 {
247 	if (!fData)
248 		return 8;
249 	return fData->maxRedirections;
250 }
251 
252 
253 const BHttpMethod&
254 BHttpRequest::Method() const noexcept
255 {
256 	if (!fData)
257 		return kDefaultMethod;
258 	return fData->method;
259 }
260 
261 
262 const BHttpRequest::Body*
263 BHttpRequest::RequestBody() const noexcept
264 {
265 	if (fData && fData->requestBody)
266 		return std::addressof(*fData->requestBody);
267 	return nullptr;
268 }
269 
270 
271 bool
272 BHttpRequest::StopOnError() const noexcept
273 {
274 	if (!fData)
275 		return false;
276 	return fData->stopOnError;
277 }
278 
279 
280 bigtime_t
281 BHttpRequest::Timeout() const noexcept
282 {
283 	if (!fData)
284 		return B_INFINITE_TIMEOUT;
285 	return fData->timeout;
286 }
287 
288 
289 const BUrl&
290 BHttpRequest::Url() const noexcept
291 {
292 	if (!fData)
293 		return kDefaultUrl;
294 	return fData->url;
295 }
296 
297 
298 void
299 BHttpRequest::SetAuthentication(const BHttpAuthentication& authentication)
300 {
301 	if (!fData)
302 		fData = std::make_unique<Data>();
303 
304 	fData->authentication = authentication;
305 }
306 
307 
308 static constexpr std::array<std::string_view, 6> fReservedOptionalFieldNames
309 	= {"Host"sv, "Accept-Encoding"sv, "Connection"sv, "Content-Type"sv, "Content-Length"sv};
310 
311 
312 void
313 BHttpRequest::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 
331 void
332 BHttpRequest::SetMaxRedirections(uint8 maxRedirections)
333 {
334 	if (!fData)
335 		fData = std::make_unique<Data>();
336 	fData->maxRedirections = maxRedirections;
337 }
338 
339 
340 void
341 BHttpRequest::SetMethod(const BHttpMethod& method)
342 {
343 	if (!fData)
344 		fData = std::make_unique<Data>();
345 	fData->method = method;
346 }
347 
348 
349 void
350 BHttpRequest::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 
376 void
377 BHttpRequest::SetStopOnError(bool stopOnError)
378 {
379 	if (!fData)
380 		fData = std::make_unique<Data>();
381 	fData->stopOnError = stopOnError;
382 }
383 
384 
385 void
386 BHttpRequest::SetTimeout(bigtime_t timeout)
387 {
388 	if (!fData)
389 		fData = std::make_unique<Data>();
390 	fData->timeout = timeout;
391 }
392 
393 
394 void
395 BHttpRequest::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 
413 void
414 BHttpRequest::ClearAuthentication() noexcept
415 {
416 	if (fData)
417 		fData->authentication = std::nullopt;
418 }
419 
420 
421 std::unique_ptr<BDataIO>
422 BHttpRequest::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 
433 BString
434 BHttpRequest::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 */
449 bool
450 BHttpRequest::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 */
465 void
466 BHttpRequest::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