1 /* 2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2016-2023, Andrew Lindesay <apl@lindesay.co.nz>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 #include "WebAppInterface.h" 7 8 #include <Application.h> 9 #include <Message.h> 10 #include <Url.h> 11 12 #include <AutoDeleter.h> 13 #include <AutoLocker.h> 14 #include <HttpHeaders.h> 15 #include <HttpRequest.h> 16 #include <Json.h> 17 #include <JsonTextWriter.h> 18 #include <JsonMessageWriter.h> 19 #include <UrlContext.h> 20 #include <UrlProtocolListener.h> 21 #include <UrlProtocolRoster.h> 22 23 #include "DataIOUtils.h" 24 #include "HaikuDepotConstants.h" 25 #include "JwtTokenHelper.h" 26 #include "Logger.h" 27 #include "ServerSettings.h" 28 #include "ServerHelper.h" 29 30 31 using namespace BPrivate::Network; 32 33 34 #define BASEURL_DEFAULT "https://depot.haiku-os.org" 35 #define USERAGENT_FALLBACK_VERSION "0.0.0" 36 #define PROTOCOL_NAME "post-json" 37 #define LOG_PAYLOAD_LIMIT 8192 38 39 40 class ProtocolListener : public BUrlProtocolListener { 41 public: 42 ProtocolListener() 43 { 44 } 45 46 virtual ~ProtocolListener() 47 { 48 } 49 50 virtual void ConnectionOpened(BUrlRequest* caller) 51 { 52 } 53 54 virtual void HostnameResolved(BUrlRequest* caller, const char* ip) 55 { 56 } 57 58 virtual void ResponseStarted(BUrlRequest* caller) 59 { 60 } 61 62 virtual void HeadersReceived(BUrlRequest* caller) 63 { 64 } 65 66 virtual void BytesWritten(BUrlRequest* caller, size_t bytesWritten) 67 { 68 } 69 70 virtual void DownloadProgress(BUrlRequest* caller, off_t bytesReceived, off_t bytesTotal) 71 { 72 } 73 74 virtual void UploadProgress(BUrlRequest* caller, off_t bytesSent, off_t bytesTotal) 75 { 76 } 77 78 virtual void RequestCompleted(BUrlRequest* caller, bool success) 79 { 80 } 81 82 virtual void DebugMessage(BUrlRequest* caller, 83 BUrlProtocolDebugMessage type, const char* text) 84 { 85 HDTRACE("post-json: %s", text); 86 } 87 }; 88 89 90 static BHttpRequest* 91 make_http_request(const BUrl& url, BDataIO* output, 92 BUrlProtocolListener* listener = NULL, 93 BUrlContext* context = NULL) 94 { 95 BUrlRequest* request = BUrlProtocolRoster::MakeRequest(url, output, 96 listener, context); 97 BHttpRequest* httpRequest = dynamic_cast<BHttpRequest*>(request); 98 if (httpRequest == NULL) { 99 delete request; 100 return NULL; 101 } 102 return httpRequest; 103 } 104 105 106 enum { 107 NEEDS_AUTHORIZATION = 1 << 0, 108 }; 109 110 111 WebAppInterface::WebAppInterface() 112 { 113 } 114 115 116 WebAppInterface::~WebAppInterface() 117 { 118 } 119 120 121 void 122 WebAppInterface::SetCredentials(const UserCredentials& value) 123 { 124 AutoLocker<BLocker> lock(&fLock); 125 if (fCredentials != value) { 126 fCredentials = value; 127 fAccessToken.Clear(); 128 } 129 } 130 131 132 const BString& 133 WebAppInterface::Nickname() 134 { 135 AutoLocker<BLocker> lock(&fLock); 136 return fCredentials.Nickname(); 137 } 138 139 140 status_t 141 WebAppInterface::GetChangelog(const BString& packageName, BMessage& message) 142 { 143 BMallocIO* requestEnvelopeData = new BMallocIO(); 144 // BHttpRequest later takes ownership of this. 145 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 146 147 requestEnvelopeWriter.WriteObjectStart(); 148 requestEnvelopeWriter.WriteObjectName("pkgName"); 149 requestEnvelopeWriter.WriteString(packageName.String()); 150 requestEnvelopeWriter.WriteObjectEnd(); 151 152 return _SendJsonRequest("pkg/get-pkg-changelog", 153 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 154 0, message); 155 } 156 157 158 status_t 159 WebAppInterface::RetrieveUserRatingsForPackageForDisplay( 160 const BString& packageName, 161 const BString& webAppRepositoryCode, 162 const BString& webAppRepositorySourceCode, 163 int resultOffset, int maxResults, BMessage& message) 164 { 165 // BHttpRequest later takes ownership of this. 166 BMallocIO* requestEnvelopeData = new BMallocIO(); 167 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 168 169 requestEnvelopeWriter.WriteObjectStart(); 170 requestEnvelopeWriter.WriteObjectName("pkgName"); 171 requestEnvelopeWriter.WriteString(packageName.String()); 172 requestEnvelopeWriter.WriteObjectName("offset"); 173 requestEnvelopeWriter.WriteInteger(resultOffset); 174 requestEnvelopeWriter.WriteObjectName("limit"); 175 requestEnvelopeWriter.WriteInteger(maxResults); 176 177 if (!webAppRepositorySourceCode.IsEmpty()) { 178 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 179 requestEnvelopeWriter.WriteString(webAppRepositorySourceCode); 180 } 181 182 if (!webAppRepositoryCode.IsEmpty()) { 183 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 184 requestEnvelopeWriter.WriteString(webAppRepositoryCode); 185 } 186 187 requestEnvelopeWriter.WriteObjectEnd(); 188 189 return _SendJsonRequest("user-rating/search-user-ratings", 190 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 191 0, message); 192 } 193 194 195 status_t 196 WebAppInterface::RetrieveUserRatingForPackageAndVersionByUser( 197 const BString& packageName, const BPackageVersion& version, 198 const BString& architecture, 199 const BString& webAppRepositoryCode, 200 const BString& webAppRepositorySourceCode, 201 const BString& userNickname, BMessage& message) 202 { 203 // BHttpRequest later takes ownership of this. 204 BMallocIO* requestEnvelopeData = new BMallocIO(); 205 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 206 207 requestEnvelopeWriter.WriteObjectStart(); 208 209 requestEnvelopeWriter.WriteObjectName("userNickname"); 210 requestEnvelopeWriter.WriteString(userNickname.String()); 211 requestEnvelopeWriter.WriteObjectName("pkgName"); 212 requestEnvelopeWriter.WriteString(packageName.String()); 213 requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode"); 214 requestEnvelopeWriter.WriteString(architecture.String()); 215 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 216 requestEnvelopeWriter.WriteString(webAppRepositoryCode.String()); 217 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 218 requestEnvelopeWriter.WriteString(webAppRepositorySourceCode.String()); 219 220 if (version.Major().Length() > 0) { 221 requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); 222 requestEnvelopeWriter.WriteString(version.Major().String()); 223 } 224 225 if (version.Minor().Length() > 0) { 226 requestEnvelopeWriter.WriteObjectName("pkgVersionMinor"); 227 requestEnvelopeWriter.WriteString(version.Minor().String()); 228 } 229 230 if (version.Micro().Length() > 0) { 231 requestEnvelopeWriter.WriteObjectName("pkgVersionMicro"); 232 requestEnvelopeWriter.WriteString(version.Micro().String()); 233 } 234 235 if (version.PreRelease().Length() > 0) { 236 requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease"); 237 requestEnvelopeWriter.WriteString(version.PreRelease().String()); 238 } 239 240 if (version.Revision() != 0) { 241 requestEnvelopeWriter.WriteObjectName("pkgVersionRevision"); 242 requestEnvelopeWriter.WriteInteger(version.Revision()); 243 } 244 245 requestEnvelopeWriter.WriteObjectEnd(); 246 247 return _SendJsonRequest( 248 "user-rating/get-user-rating-by-user-and-pkg-version", 249 requestEnvelopeData, 250 _LengthAndSeekToZero(requestEnvelopeData), NEEDS_AUTHORIZATION, 251 message); 252 } 253 254 255 /*! This method will fill out the supplied UserDetail object with information 256 about the user that is supplied in the credentials. Importantly it will 257 also authenticate the request with the details of the credentials and will 258 not use the credentials that are configured in 'fCredentials'. 259 */ 260 261 status_t 262 WebAppInterface::RetrieveUserDetailForCredentials( 263 const UserCredentials& credentials, BMessage& message) 264 { 265 if (!credentials.IsValid()) { 266 debugger("the credentials supplied are invalid so it is not possible " 267 "to obtain the user detail"); 268 } 269 270 status_t result = B_OK; 271 272 // authenticate the user and obtain a token to use with the latter 273 // request. 274 275 BMessage authenticateResponseEnvelopeMessage; 276 277 if (result == B_OK) { 278 result = AuthenticateUser( 279 credentials.Nickname(), 280 credentials.PasswordClear(), 281 authenticateResponseEnvelopeMessage); 282 } 283 284 AccessToken accessToken; 285 286 if (result == B_OK) 287 result = UnpackAccessToken(authenticateResponseEnvelopeMessage, accessToken); 288 289 if (result == B_OK) { 290 // BHttpRequest later takes ownership of this. 291 BMallocIO* requestEnvelopeData = new BMallocIO(); 292 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 293 294 requestEnvelopeWriter.WriteObjectStart(); 295 requestEnvelopeWriter.WriteObjectName("nickname"); 296 requestEnvelopeWriter.WriteString(credentials.Nickname().String()); 297 requestEnvelopeWriter.WriteObjectEnd(); 298 299 result = _SendJsonRequest("user/get-user", accessToken, 300 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 301 NEEDS_AUTHORIZATION, message); 302 // note that the credentials used here are passed in as args. 303 } 304 305 return result; 306 } 307 308 309 /*! This method will return the credentials for the currently authenticated 310 user. 311 */ 312 313 status_t 314 WebAppInterface::RetrieveCurrentUserDetail(BMessage& message) 315 { 316 UserCredentials credentials = _Credentials(); 317 return RetrieveUserDetailForCredentials(credentials, message); 318 } 319 320 321 /*! When the user requests user detail, the server sends back an envelope of 322 response data. This method will unpack the data into a model object. 323 \return Not B_OK if something went wrong. 324 */ 325 326 /*static*/ status_t 327 WebAppInterface::UnpackUserDetail(BMessage& responseEnvelopeMessage, 328 UserDetail& userDetail) 329 { 330 BMessage resultMessage; 331 status_t result = responseEnvelopeMessage.FindMessage( 332 "result", &resultMessage); 333 334 if (result != B_OK) { 335 HDERROR("bad response envelope missing 'result' entry"); 336 return result; 337 } 338 339 BString nickname; 340 result = resultMessage.FindString("nickname", &nickname); 341 userDetail.SetNickname(nickname); 342 343 BMessage agreementMessage; 344 if (resultMessage.FindMessage("userUsageConditionsAgreement", 345 &agreementMessage) == B_OK) { 346 BString code; 347 BDateTime agreedToTimestamp; 348 BString userUsageConditionsCode; 349 UserUsageConditionsAgreement agreement = userDetail.Agreement(); 350 bool isLatest; 351 352 if (agreementMessage.FindString("userUsageConditionsCode", 353 &userUsageConditionsCode) == B_OK) { 354 agreement.SetCode(userUsageConditionsCode); 355 } 356 357 double timestampAgreedMillis; 358 if (agreementMessage.FindDouble("timestampAgreed", 359 ×tampAgreedMillis) == B_OK) { 360 agreement.SetTimestampAgreed((uint64) timestampAgreedMillis); 361 } 362 363 if (agreementMessage.FindBool("isLatest", &isLatest) 364 == B_OK) { 365 agreement.SetIsLatest(isLatest); 366 } 367 368 userDetail.SetAgreement(agreement); 369 } 370 371 return result; 372 } 373 374 375 /*! When an authentication API call is made, the response (if successful) will 376 return an access token in the response. This method will take the response 377 from the server and will parse out the access token data into the supplied 378 object. 379 */ 380 381 /*static*/ status_t 382 WebAppInterface::UnpackAccessToken(BMessage& responseEnvelopeMessage, 383 AccessToken& accessToken) 384 { 385 status_t result; 386 387 BMessage resultMessage; 388 result = responseEnvelopeMessage.FindMessage( 389 "result", &resultMessage); 390 391 if (result != B_OK) { 392 HDERROR("bad response envelope missing 'result' entry"); 393 return result; 394 } 395 396 BString token; 397 result = resultMessage.FindString("token", &token); 398 399 if (result != B_OK || token.IsEmpty()) { 400 HDINFO("failure to authenticate"); 401 return B_PERMISSION_DENIED; 402 } 403 404 // The token should be present in three parts; the header, the claims and 405 // then a digital signature. The logic here wants to extract some data 406 // from the claims part. 407 408 BMessage claimsMessage; 409 result = JwtTokenHelper::ParseClaims(token, claimsMessage); 410 411 if (Logger::IsTraceEnabled()) { 412 HDTRACE("start; token claims..."); 413 claimsMessage.PrintToStream(); 414 HDTRACE("...end; token claims"); 415 } 416 417 if (B_OK == result) { 418 accessToken.SetToken(token); 419 accessToken.SetExpiryTimestamp(0); 420 421 double expiryTimestampDouble; 422 423 // The claims should have parsed but it could transpire that there is 424 // no expiry. This should not be the case, but it is theoretically 425 // possible. 426 427 if (claimsMessage.FindDouble("exp", &expiryTimestampDouble) == B_OK) 428 accessToken.SetExpiryTimestamp(1000 * static_cast<uint64>(expiryTimestampDouble)); 429 } 430 431 return result; 432 } 433 434 435 /*! \brief Returns data relating to the user usage conditions 436 437 \param code defines the version of the data to return or if empty then the 438 latest is returned. 439 440 This method will go to the server and get details relating to the user usage 441 conditions. It does this in two API calls; first gets the details (the 442 minimum age) and in the second call, the text of the conditions is returned. 443 */ 444 445 status_t 446 WebAppInterface::RetrieveUserUsageConditions(const BString& code, 447 UserUsageConditions& conditions) 448 { 449 BMessage responseEnvelopeMessage; 450 status_t result = _RetrieveUserUsageConditionsMeta(code, 451 responseEnvelopeMessage); 452 453 if (result != B_OK) 454 return result; 455 456 BMessage resultMessage; 457 if (responseEnvelopeMessage.FindMessage("result", &resultMessage) != B_OK) { 458 HDERROR("bad response envelope missing 'result' entry"); 459 return B_BAD_DATA; 460 } 461 462 BString metaDataCode; 463 double metaDataMinimumAge; 464 BString copyMarkdown; 465 466 if ( (resultMessage.FindString("code", &metaDataCode) != B_OK) 467 || (resultMessage.FindDouble( 468 "minimumAge", &metaDataMinimumAge) != B_OK) ) { 469 HDERROR("unexpected response from server with missing user usage " 470 "conditions data"); 471 return B_BAD_DATA; 472 } 473 474 BMallocIO* copyMarkdownData = new BMallocIO(); 475 result = _RetrieveUserUsageConditionsCopy(metaDataCode, copyMarkdownData); 476 477 if (result != B_OK) 478 return result; 479 480 conditions.SetCode(metaDataCode); 481 conditions.SetMinimumAge(metaDataMinimumAge); 482 conditions.SetCopyMarkdown( 483 BString(static_cast<const char*>(copyMarkdownData->Buffer()), 484 copyMarkdownData->BufferLength())); 485 486 return B_OK; 487 } 488 489 490 status_t 491 WebAppInterface::AgreeUserUsageConditions(const BString& code, 492 BMessage& responsePayload) 493 { 494 BMallocIO* requestEnvelopeData = new BMallocIO(); 495 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 496 497 requestEnvelopeWriter.WriteObjectStart(); 498 requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode"); 499 requestEnvelopeWriter.WriteString(code.String()); 500 requestEnvelopeWriter.WriteObjectName("nickname"); 501 requestEnvelopeWriter.WriteString(Nickname()); 502 requestEnvelopeWriter.WriteObjectEnd(); 503 504 // now fetch this information into an object. 505 506 return _SendJsonRequest("user/agree-user-usage-conditions", 507 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 508 NEEDS_AUTHORIZATION, responsePayload); 509 } 510 511 512 status_t 513 WebAppInterface::_RetrieveUserUsageConditionsMeta(const BString& code, 514 BMessage& message) 515 { 516 BMallocIO* requestEnvelopeData = new BMallocIO(); 517 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 518 519 requestEnvelopeWriter.WriteObjectStart(); 520 521 if (!code.IsEmpty()) { 522 requestEnvelopeWriter.WriteObjectName("code"); 523 requestEnvelopeWriter.WriteString(code.String()); 524 } 525 526 requestEnvelopeWriter.WriteObjectEnd(); 527 528 // now fetch this information into an object. 529 530 return _SendJsonRequest("user/get-user-usage-conditions", 531 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 532 0, message); 533 } 534 535 536 status_t 537 WebAppInterface::_RetrieveUserUsageConditionsCopy(const BString& code, 538 BDataIO* stream) 539 { 540 return _SendRawGetRequest( 541 BString("/__user/usageconditions/") << code << "/document.md", 542 stream); 543 } 544 545 546 status_t 547 WebAppInterface::CreateUserRating(const BString& packageName, 548 const BPackageVersion& version, 549 const BString& architecture, 550 const BString& webAppRepositoryCode, 551 const BString& webAppRepositorySourceCode, 552 const BString& languageCode, const BString& comment, 553 const BString& stability, int rating, BMessage& message) 554 { 555 BMallocIO* requestEnvelopeData = new BMallocIO(); 556 // BHttpRequest later takes ownership of this. 557 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 558 559 requestEnvelopeWriter.WriteObjectStart(); 560 requestEnvelopeWriter.WriteObjectName("pkgName"); 561 requestEnvelopeWriter.WriteString(packageName.String()); 562 requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode"); 563 requestEnvelopeWriter.WriteString(architecture.String()); 564 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 565 requestEnvelopeWriter.WriteString(webAppRepositoryCode.String()); 566 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 567 requestEnvelopeWriter.WriteString(webAppRepositorySourceCode.String()); 568 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 569 requestEnvelopeWriter.WriteString(languageCode.String()); 570 requestEnvelopeWriter.WriteObjectName("pkgVersionType"); 571 requestEnvelopeWriter.WriteString("SPECIFIC"); 572 requestEnvelopeWriter.WriteObjectName("userNickname"); 573 requestEnvelopeWriter.WriteString(Nickname()); 574 575 if (!version.Major().IsEmpty()) { 576 requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); 577 requestEnvelopeWriter.WriteString(version.Major()); 578 } 579 580 if (!version.Minor().IsEmpty()) { 581 requestEnvelopeWriter.WriteObjectName("pkgVersionMinor"); 582 requestEnvelopeWriter.WriteString(version.Minor()); 583 } 584 585 if (!version.Micro().IsEmpty()) { 586 requestEnvelopeWriter.WriteObjectName("pkgVersionMicro"); 587 requestEnvelopeWriter.WriteString(version.Micro()); 588 } 589 590 if (!version.PreRelease().IsEmpty()) { 591 requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease"); 592 requestEnvelopeWriter.WriteString(version.PreRelease()); 593 } 594 595 if (version.Revision() != 0) { 596 requestEnvelopeWriter.WriteObjectName("pkgVersionRevision"); 597 requestEnvelopeWriter.WriteInteger(version.Revision()); 598 } 599 600 if (rating > 0.0f) { 601 requestEnvelopeWriter.WriteObjectName("rating"); 602 requestEnvelopeWriter.WriteInteger(rating); 603 } 604 605 if (stability.Length() > 0) { 606 requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); 607 requestEnvelopeWriter.WriteString(stability); 608 } 609 610 if (comment.Length() > 0) { 611 requestEnvelopeWriter.WriteObjectName("comment"); 612 requestEnvelopeWriter.WriteString(comment.String()); 613 } 614 615 requestEnvelopeWriter.WriteObjectEnd(); 616 617 return _SendJsonRequest("user-rating/create-user-rating", 618 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 619 NEEDS_AUTHORIZATION, message); 620 } 621 622 623 status_t 624 WebAppInterface::UpdateUserRating(const BString& ratingID, 625 const BString& languageCode, const BString& comment, 626 const BString& stability, int rating, bool active, BMessage& message) 627 { 628 BMallocIO* requestEnvelopeData = new BMallocIO(); 629 // BHttpRequest later takes ownership of this. 630 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 631 632 requestEnvelopeWriter.WriteObjectStart(); 633 634 requestEnvelopeWriter.WriteObjectName("code"); 635 requestEnvelopeWriter.WriteString(ratingID.String()); 636 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 637 requestEnvelopeWriter.WriteString(languageCode.String()); 638 requestEnvelopeWriter.WriteObjectName("active"); 639 requestEnvelopeWriter.WriteBoolean(active); 640 641 requestEnvelopeWriter.WriteObjectName("filter"); 642 requestEnvelopeWriter.WriteArrayStart(); 643 requestEnvelopeWriter.WriteString("ACTIVE"); 644 requestEnvelopeWriter.WriteString("NATURALLANGUAGE"); 645 requestEnvelopeWriter.WriteString("USERRATINGSTABILITY"); 646 requestEnvelopeWriter.WriteString("COMMENT"); 647 requestEnvelopeWriter.WriteString("RATING"); 648 requestEnvelopeWriter.WriteArrayEnd(); 649 650 if (rating >= 0) { 651 requestEnvelopeWriter.WriteObjectName("rating"); 652 requestEnvelopeWriter.WriteInteger(rating); 653 } 654 655 if (stability.Length() > 0) { 656 requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); 657 requestEnvelopeWriter.WriteString(stability); 658 } 659 660 if (comment.Length() > 0) { 661 requestEnvelopeWriter.WriteObjectName("comment"); 662 requestEnvelopeWriter.WriteString(comment); 663 } 664 665 requestEnvelopeWriter.WriteObjectEnd(); 666 667 return _SendJsonRequest("user-rating/update-user-rating", 668 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 669 NEEDS_AUTHORIZATION, message); 670 } 671 672 673 /*! This method will call to the server to get a screenshot that will fit into 674 the specified width and height. 675 */ 676 677 status_t 678 WebAppInterface::RetrieveScreenshot(const BString& code, 679 int32 width, int32 height, BDataIO* stream) 680 { 681 return _SendRawGetRequest( 682 BString("/__pkgscreenshot/") << code << ".png" << "?tw=" 683 << width << "&th=" << height, stream); 684 } 685 686 687 status_t 688 WebAppInterface::RequestCaptcha(BMessage& message) 689 { 690 BMallocIO* requestEnvelopeData = new BMallocIO(); 691 // BHttpRequest later takes ownership of this. 692 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 693 694 requestEnvelopeWriter.WriteObjectStart(); 695 requestEnvelopeWriter.WriteObjectEnd(); 696 697 return _SendJsonRequest("captcha/generate-captcha", 698 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 699 0, message); 700 } 701 702 703 status_t 704 WebAppInterface::CreateUser(const BString& nickName, 705 const BString& passwordClear, const BString& email, 706 const BString& captchaToken, const BString& captchaResponse, 707 const BString& languageCode, const BString& userUsageConditionsCode, 708 BMessage& message) 709 { 710 // BHttpRequest later takes ownership of this. 711 BMallocIO* requestEnvelopeData = new BMallocIO(); 712 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 713 714 requestEnvelopeWriter.WriteObjectStart(); 715 716 requestEnvelopeWriter.WriteObjectName("nickname"); 717 requestEnvelopeWriter.WriteString(nickName.String()); 718 requestEnvelopeWriter.WriteObjectName("passwordClear"); 719 requestEnvelopeWriter.WriteString(passwordClear.String()); 720 requestEnvelopeWriter.WriteObjectName("captchaToken"); 721 requestEnvelopeWriter.WriteString(captchaToken.String()); 722 requestEnvelopeWriter.WriteObjectName("captchaResponse"); 723 requestEnvelopeWriter.WriteString(captchaResponse.String()); 724 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 725 requestEnvelopeWriter.WriteString(languageCode.String()); 726 requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode"); 727 requestEnvelopeWriter.WriteString(userUsageConditionsCode.String()); 728 729 if (!email.IsEmpty()) { 730 requestEnvelopeWriter.WriteObjectName("email"); 731 requestEnvelopeWriter.WriteString(email.String()); 732 } 733 734 requestEnvelopeWriter.WriteObjectEnd(); 735 736 return _SendJsonRequest("user/create-user", requestEnvelopeData, 737 _LengthAndSeekToZero(requestEnvelopeData), 0, message); 738 } 739 740 741 /*! This method will authenticate the user set in the credentials and will 742 retain the resultant access token for authenticating any latter API calls. 743 */ 744 745 status_t 746 WebAppInterface::AuthenticateUserRetainingAccessToken() 747 { 748 UserCredentials userCredentials = _Credentials(); 749 750 if (!userCredentials.IsValid()) { 751 HDINFO("unable to get a new access token as there are no credentials"); 752 return B_NOT_INITIALIZED; 753 } 754 755 return _AuthenticateUserRetainingAccessToken(userCredentials.Nickname(), 756 userCredentials.PasswordClear()); 757 } 758 759 760 status_t 761 WebAppInterface::_AuthenticateUserRetainingAccessToken(const BString& nickName, 762 const BString& passwordClear) { 763 AutoLocker<BLocker> lock(&fLock); 764 765 fAccessToken.Clear(); 766 767 BMessage responseEnvelopeMessage; 768 status_t result = AuthenticateUser(nickName, passwordClear, responseEnvelopeMessage); 769 770 AccessToken accessToken; 771 772 if (result == B_OK) 773 result = UnpackAccessToken(responseEnvelopeMessage, accessToken); 774 775 if (result == B_OK) 776 fAccessToken = accessToken; 777 778 return result; 779 } 780 781 782 status_t 783 WebAppInterface::AuthenticateUser(const BString& nickName, 784 const BString& passwordClear, BMessage& message) 785 { 786 BMallocIO* requestEnvelopeData = new BMallocIO(); 787 // BHttpRequest later takes ownership of this. 788 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 789 790 requestEnvelopeWriter.WriteObjectStart(); 791 792 requestEnvelopeWriter.WriteObjectName("nickname"); 793 requestEnvelopeWriter.WriteString(nickName.String()); 794 requestEnvelopeWriter.WriteObjectName("passwordClear"); 795 requestEnvelopeWriter.WriteString(passwordClear.String()); 796 797 requestEnvelopeWriter.WriteObjectEnd(); 798 799 return _SendJsonRequest("user/authenticate-user", 800 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 801 0, message); 802 } 803 804 805 status_t 806 WebAppInterface::IncrementViewCounter(const PackageInfoRef package, 807 const DepotInfoRef depot, BMessage& message) 808 { 809 BMallocIO* requestEnvelopeData = new BMallocIO(); 810 // BHttpRequest later takes ownership of this. 811 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 812 813 requestEnvelopeWriter.WriteObjectStart(); 814 815 requestEnvelopeWriter.WriteObjectName("architectureCode"); 816 requestEnvelopeWriter.WriteString(package->Architecture()); 817 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 818 requestEnvelopeWriter.WriteString(depot->WebAppRepositoryCode()); 819 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 820 requestEnvelopeWriter.WriteString(depot->WebAppRepositorySourceCode()); 821 requestEnvelopeWriter.WriteObjectName("name"); 822 requestEnvelopeWriter.WriteString(package->Name()); 823 824 const BPackageVersion version = package->Version(); 825 if (!version.Major().IsEmpty()) { 826 requestEnvelopeWriter.WriteObjectName("major"); 827 requestEnvelopeWriter.WriteString(version.Major()); 828 } 829 if (!version.Minor().IsEmpty()) { 830 requestEnvelopeWriter.WriteObjectName("minor"); 831 requestEnvelopeWriter.WriteString(version.Minor()); 832 } 833 if (!version.Micro().IsEmpty()) { 834 requestEnvelopeWriter.WriteObjectName("micro"); 835 requestEnvelopeWriter.WriteString(version.Micro()); 836 } 837 if (!version.PreRelease().IsEmpty()) { 838 requestEnvelopeWriter.WriteObjectName("preRelease"); 839 requestEnvelopeWriter.WriteString(version.PreRelease()); 840 } 841 if (version.Revision() != 0) { 842 requestEnvelopeWriter.WriteObjectName("revision"); 843 requestEnvelopeWriter.WriteInteger( 844 static_cast<int64>(version.Revision())); 845 } 846 847 requestEnvelopeWriter.WriteObjectEnd(); 848 849 return _SendJsonRequest("pkg/increment-view-counter", 850 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 851 0, message); 852 } 853 854 855 status_t 856 WebAppInterface::RetrievePasswordRequirements( 857 PasswordRequirements& passwordRequirements) 858 { 859 BMessage responseEnvelopeMessage; 860 status_t result = _RetrievePasswordRequirementsMeta( 861 responseEnvelopeMessage); 862 863 if (result != B_OK) 864 return result; 865 866 BMessage resultMessage; 867 868 result = responseEnvelopeMessage.FindMessage("result", &resultMessage); 869 870 if (result != B_OK) { 871 HDERROR("bad response envelope missing 'result' entry"); 872 return result; 873 } 874 875 double value; 876 877 if (resultMessage.FindDouble("minPasswordLength", &value) == B_OK) 878 passwordRequirements.SetMinPasswordLength((uint32) value); 879 880 if (resultMessage.FindDouble("minPasswordUppercaseChar", &value) == B_OK) 881 passwordRequirements.SetMinPasswordUppercaseChar((uint32) value); 882 883 if (resultMessage.FindDouble("minPasswordDigitsChar", &value) == B_OK) 884 passwordRequirements.SetMinPasswordDigitsChar((uint32) value); 885 886 return result; 887 } 888 889 890 status_t 891 WebAppInterface::_RetrievePasswordRequirementsMeta(BMessage& message) 892 { 893 BMallocIO* requestEnvelopeData = new BMallocIO(); 894 // BHttpRequest later takes ownership of this. 895 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 896 897 requestEnvelopeWriter.WriteObjectStart(); 898 requestEnvelopeWriter.WriteObjectEnd(); 899 900 return _SendJsonRequest("user/get-password-requirements", 901 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 902 0, message); 903 } 904 905 906 /*! JSON-RPC invocations return a response. The response may be either 907 a result or it may be an error depending on the response structure. 908 If it is an error then there may be additional detail that is the 909 error code and message. This method will extract the error code 910 from the response. This method will return 0 if the payload does 911 not look like an error. 912 */ 913 914 /*static*/ int32 915 WebAppInterface::ErrorCodeFromResponse(BMessage& responseEnvelopeMessage) 916 { 917 BMessage error; 918 double code; 919 920 if (responseEnvelopeMessage.FindMessage("error", &error) == B_OK 921 && error.FindDouble("code", &code) == B_OK) { 922 return (int32) code; 923 } 924 925 return 0; 926 } 927 928 929 // #pragma mark - private 930 931 932 status_t 933 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 934 BPositionIO* requestData, size_t requestDataSize, uint32 flags, 935 BMessage& reply) 936 { 937 bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; 938 AccessToken accessToken; 939 940 if (needsAuthorization) 941 accessToken = _ObtainValidAccessToken(); 942 943 return _SendJsonRequest(urlPathComponents, accessToken, requestData, 944 requestDataSize, flags, reply); 945 } 946 947 948 /*static*/ status_t 949 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 950 const AccessToken& accessToken, BPositionIO* requestData, 951 size_t requestDataSize, uint32 flags, BMessage& reply) 952 { 953 if (requestDataSize == 0) { 954 HDINFO("%s; empty request payload", PROTOCOL_NAME); 955 return B_ERROR; 956 } 957 958 if (!ServerHelper::IsNetworkAvailable()) { 959 HDDEBUG("%s; dropping request to ...[%s] as network is not" 960 " available", PROTOCOL_NAME, urlPathComponents); 961 delete requestData; 962 return HD_NETWORK_INACCESSIBLE; 963 } 964 965 if (ServerSettings::IsClientTooOld()) { 966 HDDEBUG("%s; dropping request to ...[%s] as client is too old", 967 PROTOCOL_NAME, urlPathComponents); 968 delete requestData; 969 return HD_CLIENT_TOO_OLD; 970 } 971 972 bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; 973 974 if (needsAuthorization && !accessToken.IsValid()) { 975 HDDEBUG("%s; dropping request to ...[%s] as access token is not valid", 976 PROTOCOL_NAME, urlPathComponents); 977 delete requestData; 978 return B_NOT_ALLOWED; 979 } 980 981 BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v2/") 982 << urlPathComponents); 983 HDDEBUG("%s; will make request to [%s]", PROTOCOL_NAME, 984 url.UrlString().String()); 985 986 // If the request payload is logged then it must be copied to local memory 987 // from the stream. This then requires that the request data is then 988 // delivered from memory. 989 990 if (Logger::IsTraceEnabled()) { 991 HDLOGPREFIX(LOG_LEVEL_TRACE) 992 printf("%s request; ", PROTOCOL_NAME); 993 _LogPayload(requestData, requestDataSize); 994 printf("\n"); 995 } 996 997 ProtocolListener listener; 998 BUrlContext context; 999 1000 BHttpHeaders headers; 1001 headers.AddHeader("Content-Type", "application/json"); 1002 headers.AddHeader("Accept", "application/json"); 1003 ServerSettings::AugmentHeaders(headers); 1004 1005 BHttpRequest* request = make_http_request(url, NULL, &listener, &context); 1006 ObjectDeleter<BHttpRequest> _(request); 1007 if (request == NULL) 1008 return B_ERROR; 1009 request->SetMethod(B_HTTP_POST); 1010 request->SetHeaders(headers); 1011 1012 if (needsAuthorization) { 1013 BHttpAuthentication authentication; 1014 authentication.SetMethod(B_HTTP_AUTHENTICATION_BEARER); 1015 authentication.SetToken(accessToken.Token()); 1016 context.AddAuthentication(url, authentication); 1017 } 1018 1019 request->AdoptInputData(requestData, requestDataSize); 1020 1021 BMallocIO replyData; 1022 request->SetOutput(&replyData); 1023 1024 thread_id thread = request->Run(); 1025 wait_for_thread(thread, NULL); 1026 1027 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 1028 request->Result()); 1029 1030 int32 statusCode = result.StatusCode(); 1031 1032 HDDEBUG("%s; did receive http-status [%" B_PRId32 "] from [%s]", 1033 PROTOCOL_NAME, statusCode, url.UrlString().String()); 1034 1035 switch (statusCode) { 1036 case B_HTTP_STATUS_OK: 1037 break; 1038 1039 case B_HTTP_STATUS_PRECONDITION_FAILED: 1040 ServerHelper::NotifyClientTooOld(result.Headers()); 1041 return HD_CLIENT_TOO_OLD; 1042 1043 default: 1044 HDERROR("%s; request to endpoint [.../%s] failed with http " 1045 "status [%" B_PRId32 "]\n", PROTOCOL_NAME, urlPathComponents, 1046 statusCode); 1047 return B_ERROR; 1048 } 1049 1050 replyData.Seek(0, SEEK_SET); 1051 1052 if (Logger::IsTraceEnabled()) { 1053 HDLOGPREFIX(LOG_LEVEL_TRACE) 1054 printf("%s; response; ", PROTOCOL_NAME); 1055 _LogPayload(&replyData, replyData.BufferLength()); 1056 printf("\n"); 1057 } 1058 1059 BJsonMessageWriter jsonMessageWriter(reply); 1060 BJson::Parse(&replyData, &jsonMessageWriter); 1061 status_t status = jsonMessageWriter.ErrorStatus(); 1062 1063 if (Logger::IsTraceEnabled() && status == B_BAD_DATA) { 1064 BString resultString(static_cast<const char *>(replyData.Buffer()), 1065 replyData.BufferLength()); 1066 HDERROR("Parser choked on JSON:\n%s", resultString.String()); 1067 } 1068 return status; 1069 } 1070 1071 1072 status_t 1073 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 1074 const BString& jsonString, uint32 flags, BMessage& reply) 1075 { 1076 // gets 'adopted' by the subsequent http request. 1077 BMemoryIO* data = new BMemoryIO(jsonString.String(), 1078 jsonString.Length() - 1); 1079 1080 return _SendJsonRequest(urlPathComponents, data, jsonString.Length() - 1, 1081 flags, reply); 1082 } 1083 1084 1085 status_t 1086 WebAppInterface::_SendRawGetRequest(const BString urlPathComponents, 1087 BDataIO* stream) 1088 { 1089 BUrl url = ServerSettings::CreateFullUrl(urlPathComponents); 1090 1091 HDDEBUG("http-get; will make request to [%s]", 1092 url.UrlString().String()); 1093 1094 ProtocolListener listener; 1095 1096 BHttpHeaders headers; 1097 ServerSettings::AugmentHeaders(headers); 1098 1099 BHttpRequest *request = make_http_request(url, stream, &listener); 1100 ObjectDeleter<BHttpRequest> _(request); 1101 if (request == NULL) 1102 return B_ERROR; 1103 request->SetMethod(B_HTTP_GET); 1104 request->SetHeaders(headers); 1105 1106 thread_id thread = request->Run(); 1107 wait_for_thread(thread, NULL); 1108 1109 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 1110 request->Result()); 1111 1112 int32 statusCode = result.StatusCode(); 1113 1114 HDDEBUG("http-get; did receive http-status [%" B_PRId32 "] from [%s]", 1115 statusCode, url.UrlString().String()); 1116 1117 if (statusCode == 200) 1118 return B_OK; 1119 1120 HDERROR("failed to get data from '%s': %" B_PRIi32 "", 1121 url.UrlString().String(), statusCode); 1122 return B_ERROR; 1123 } 1124 1125 1126 void 1127 WebAppInterface::_LogPayload(BPositionIO* requestData, size_t size) 1128 { 1129 off_t requestDataOffset = requestData->Position(); 1130 char buffer[LOG_PAYLOAD_LIMIT]; 1131 1132 if (size > LOG_PAYLOAD_LIMIT) 1133 size = LOG_PAYLOAD_LIMIT; 1134 1135 if (B_OK != requestData->ReadExactly(buffer, size)) { 1136 printf("%s; error logging payload", PROTOCOL_NAME); 1137 } else { 1138 for (uint32 i = 0; i < size; i++) { 1139 bool esc = buffer[i] > 126 || 1140 (buffer[i] < 0x20 && buffer[i] != 0x0a); 1141 1142 if (esc) 1143 printf("\\u%02x", buffer[i]); 1144 else 1145 putchar(buffer[i]); 1146 } 1147 1148 if (size == LOG_PAYLOAD_LIMIT) 1149 printf("...(continues)"); 1150 } 1151 1152 requestData->Seek(requestDataOffset, SEEK_SET); 1153 } 1154 1155 1156 /*! This will get the position of the data to get the length an then sets the 1157 offset to zero so that it can be re-read for reading the payload in to log 1158 or send. 1159 */ 1160 1161 off_t 1162 WebAppInterface::_LengthAndSeekToZero(BPositionIO* data) 1163 { 1164 off_t dataSize = data->Position(); 1165 data->Seek(0, SEEK_SET); 1166 return dataSize; 1167 } 1168 1169 1170 UserCredentials 1171 WebAppInterface::_Credentials() 1172 { 1173 AutoLocker<BLocker> lock(&fLock); 1174 return fCredentials; 1175 } 1176 1177 1178 AccessToken 1179 WebAppInterface::_ObtainValidAccessToken() 1180 { 1181 AutoLocker<BLocker> lock(&fLock); 1182 1183 uint64 now = static_cast<uint64>(time(NULL)) * 1000; 1184 1185 if (!fAccessToken.IsValid(now)) { 1186 HDINFO("clearing cached access token as it is no longer valid"); 1187 fAccessToken.Clear(); 1188 } 1189 1190 if (!fAccessToken.IsValid()) { 1191 HDINFO("no cached access token present; will obtain a new one"); 1192 AuthenticateUserRetainingAccessToken(); 1193 } 1194 1195 return fAccessToken; 1196 } 1197