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