1 /* 2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2016-2024, 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& naturalLanguageCode, 553 // This is the "ID" in the ICU system; the term `code` is used with the 554 // server system. 555 const BString& comment, 556 const BString& stability, int rating, BMessage& message) 557 { 558 BMallocIO* requestEnvelopeData = new BMallocIO(); 559 // BHttpRequest later takes ownership of this. 560 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 561 562 requestEnvelopeWriter.WriteObjectStart(); 563 requestEnvelopeWriter.WriteObjectName("pkgName"); 564 requestEnvelopeWriter.WriteString(packageName.String()); 565 requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode"); 566 requestEnvelopeWriter.WriteString(architecture.String()); 567 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 568 requestEnvelopeWriter.WriteString(webAppRepositoryCode.String()); 569 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 570 requestEnvelopeWriter.WriteString(webAppRepositorySourceCode.String()); 571 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 572 requestEnvelopeWriter.WriteString(naturalLanguageCode.String()); 573 requestEnvelopeWriter.WriteObjectName("pkgVersionType"); 574 requestEnvelopeWriter.WriteString("SPECIFIC"); 575 requestEnvelopeWriter.WriteObjectName("userNickname"); 576 requestEnvelopeWriter.WriteString(Nickname()); 577 578 if (!version.Major().IsEmpty()) { 579 requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); 580 requestEnvelopeWriter.WriteString(version.Major()); 581 } 582 583 if (!version.Minor().IsEmpty()) { 584 requestEnvelopeWriter.WriteObjectName("pkgVersionMinor"); 585 requestEnvelopeWriter.WriteString(version.Minor()); 586 } 587 588 if (!version.Micro().IsEmpty()) { 589 requestEnvelopeWriter.WriteObjectName("pkgVersionMicro"); 590 requestEnvelopeWriter.WriteString(version.Micro()); 591 } 592 593 if (!version.PreRelease().IsEmpty()) { 594 requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease"); 595 requestEnvelopeWriter.WriteString(version.PreRelease()); 596 } 597 598 if (version.Revision() != 0) { 599 requestEnvelopeWriter.WriteObjectName("pkgVersionRevision"); 600 requestEnvelopeWriter.WriteInteger(version.Revision()); 601 } 602 603 if (rating > 0.0f) { 604 requestEnvelopeWriter.WriteObjectName("rating"); 605 requestEnvelopeWriter.WriteInteger(rating); 606 } 607 608 if (stability.Length() > 0) { 609 requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); 610 requestEnvelopeWriter.WriteString(stability); 611 } 612 613 if (comment.Length() > 0) { 614 requestEnvelopeWriter.WriteObjectName("comment"); 615 requestEnvelopeWriter.WriteString(comment.String()); 616 } 617 618 requestEnvelopeWriter.WriteObjectEnd(); 619 620 return _SendJsonRequest("user-rating/create-user-rating", 621 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 622 NEEDS_AUTHORIZATION, message); 623 } 624 625 626 status_t 627 WebAppInterface::UpdateUserRating(const BString& ratingID, 628 const BString& naturalLanguageCode, 629 // This is the "ID" in the ICU system; the term `code` is used with the 630 // server system. 631 const BString& comment, 632 const BString& stability, int rating, bool active, BMessage& message) 633 { 634 BMallocIO* requestEnvelopeData = new BMallocIO(); 635 // BHttpRequest later takes ownership of this. 636 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 637 638 requestEnvelopeWriter.WriteObjectStart(); 639 640 requestEnvelopeWriter.WriteObjectName("code"); 641 requestEnvelopeWriter.WriteString(ratingID.String()); 642 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 643 requestEnvelopeWriter.WriteString(naturalLanguageCode.String()); 644 requestEnvelopeWriter.WriteObjectName("active"); 645 requestEnvelopeWriter.WriteBoolean(active); 646 647 requestEnvelopeWriter.WriteObjectName("filter"); 648 requestEnvelopeWriter.WriteArrayStart(); 649 requestEnvelopeWriter.WriteString("ACTIVE"); 650 requestEnvelopeWriter.WriteString("NATURALLANGUAGE"); 651 requestEnvelopeWriter.WriteString("USERRATINGSTABILITY"); 652 requestEnvelopeWriter.WriteString("COMMENT"); 653 requestEnvelopeWriter.WriteString("RATING"); 654 requestEnvelopeWriter.WriteArrayEnd(); 655 656 if (rating >= 0) { 657 requestEnvelopeWriter.WriteObjectName("rating"); 658 requestEnvelopeWriter.WriteInteger(rating); 659 } 660 661 if (stability.Length() > 0) { 662 requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); 663 requestEnvelopeWriter.WriteString(stability); 664 } 665 666 if (comment.Length() > 0) { 667 requestEnvelopeWriter.WriteObjectName("comment"); 668 requestEnvelopeWriter.WriteString(comment); 669 } 670 671 requestEnvelopeWriter.WriteObjectEnd(); 672 673 return _SendJsonRequest("user-rating/update-user-rating", 674 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 675 NEEDS_AUTHORIZATION, message); 676 } 677 678 679 /*! This method will call to the server to get a screenshot that will fit into 680 the specified width and height. 681 */ 682 683 status_t 684 WebAppInterface::RetrieveScreenshot(const BString& code, 685 int32 width, int32 height, BDataIO* stream) 686 { 687 return _SendRawGetRequest( 688 BString("/__pkgscreenshot/") << code << ".png" << "?tw=" 689 << width << "&th=" << height, stream); 690 } 691 692 693 status_t 694 WebAppInterface::RequestCaptcha(BMessage& message) 695 { 696 BMallocIO* requestEnvelopeData = new BMallocIO(); 697 // BHttpRequest later takes ownership of this. 698 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 699 700 requestEnvelopeWriter.WriteObjectStart(); 701 requestEnvelopeWriter.WriteObjectEnd(); 702 703 return _SendJsonRequest("captcha/generate-captcha", 704 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 705 0, message); 706 } 707 708 709 status_t 710 WebAppInterface::CreateUser(const BString& nickName, 711 const BString& passwordClear, 712 const BString& email, 713 const BString& captchaToken, 714 const BString& captchaResponse, 715 const BString& naturalLanguageCode, 716 // This is the "ID" in the ICU system; the term `code` is used with the 717 // server system. 718 const BString& userUsageConditionsCode, 719 BMessage& message) 720 { 721 // BHttpRequest later takes ownership of this. 722 BMallocIO* requestEnvelopeData = new BMallocIO(); 723 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 724 725 requestEnvelopeWriter.WriteObjectStart(); 726 727 requestEnvelopeWriter.WriteObjectName("nickname"); 728 requestEnvelopeWriter.WriteString(nickName.String()); 729 requestEnvelopeWriter.WriteObjectName("passwordClear"); 730 requestEnvelopeWriter.WriteString(passwordClear.String()); 731 requestEnvelopeWriter.WriteObjectName("captchaToken"); 732 requestEnvelopeWriter.WriteString(captchaToken.String()); 733 requestEnvelopeWriter.WriteObjectName("captchaResponse"); 734 requestEnvelopeWriter.WriteString(captchaResponse.String()); 735 requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); 736 requestEnvelopeWriter.WriteString(naturalLanguageCode.String()); 737 requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode"); 738 requestEnvelopeWriter.WriteString(userUsageConditionsCode.String()); 739 740 if (!email.IsEmpty()) { 741 requestEnvelopeWriter.WriteObjectName("email"); 742 requestEnvelopeWriter.WriteString(email.String()); 743 } 744 745 requestEnvelopeWriter.WriteObjectEnd(); 746 747 return _SendJsonRequest("user/create-user", requestEnvelopeData, 748 _LengthAndSeekToZero(requestEnvelopeData), 0, message); 749 } 750 751 752 /*! This method will authenticate the user set in the credentials and will 753 retain the resultant access token for authenticating any latter API calls. 754 */ 755 756 status_t 757 WebAppInterface::AuthenticateUserRetainingAccessToken() 758 { 759 UserCredentials userCredentials = _Credentials(); 760 761 if (!userCredentials.IsValid()) { 762 HDINFO("unable to get a new access token as there are no credentials"); 763 return B_NOT_INITIALIZED; 764 } 765 766 return _AuthenticateUserRetainingAccessToken(userCredentials.Nickname(), 767 userCredentials.PasswordClear()); 768 } 769 770 771 status_t 772 WebAppInterface::_AuthenticateUserRetainingAccessToken(const BString& nickName, 773 const BString& passwordClear) { 774 AutoLocker<BLocker> lock(&fLock); 775 776 fAccessToken.Clear(); 777 778 BMessage responseEnvelopeMessage; 779 status_t result = AuthenticateUser(nickName, passwordClear, responseEnvelopeMessage); 780 781 AccessToken accessToken; 782 783 if (result == B_OK) 784 result = UnpackAccessToken(responseEnvelopeMessage, accessToken); 785 786 if (result == B_OK) 787 fAccessToken = accessToken; 788 789 return result; 790 } 791 792 793 status_t 794 WebAppInterface::AuthenticateUser(const BString& nickName, 795 const BString& passwordClear, BMessage& message) 796 { 797 BMallocIO* requestEnvelopeData = new BMallocIO(); 798 // BHttpRequest later takes ownership of this. 799 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 800 801 requestEnvelopeWriter.WriteObjectStart(); 802 803 requestEnvelopeWriter.WriteObjectName("nickname"); 804 requestEnvelopeWriter.WriteString(nickName.String()); 805 requestEnvelopeWriter.WriteObjectName("passwordClear"); 806 requestEnvelopeWriter.WriteString(passwordClear.String()); 807 808 requestEnvelopeWriter.WriteObjectEnd(); 809 810 return _SendJsonRequest("user/authenticate-user", 811 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 812 0, message); 813 } 814 815 816 status_t 817 WebAppInterface::IncrementViewCounter(const PackageInfoRef package, 818 const DepotInfoRef depot, BMessage& message) 819 { 820 BMallocIO* requestEnvelopeData = new BMallocIO(); 821 // BHttpRequest later takes ownership of this. 822 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 823 824 requestEnvelopeWriter.WriteObjectStart(); 825 826 requestEnvelopeWriter.WriteObjectName("architectureCode"); 827 requestEnvelopeWriter.WriteString(package->Architecture()); 828 requestEnvelopeWriter.WriteObjectName("repositoryCode"); 829 requestEnvelopeWriter.WriteString(depot->WebAppRepositoryCode()); 830 requestEnvelopeWriter.WriteObjectName("repositorySourceCode"); 831 requestEnvelopeWriter.WriteString(depot->WebAppRepositorySourceCode()); 832 requestEnvelopeWriter.WriteObjectName("name"); 833 requestEnvelopeWriter.WriteString(package->Name()); 834 835 const BPackageVersion version = package->Version(); 836 if (!version.Major().IsEmpty()) { 837 requestEnvelopeWriter.WriteObjectName("major"); 838 requestEnvelopeWriter.WriteString(version.Major()); 839 } 840 if (!version.Minor().IsEmpty()) { 841 requestEnvelopeWriter.WriteObjectName("minor"); 842 requestEnvelopeWriter.WriteString(version.Minor()); 843 } 844 if (!version.Micro().IsEmpty()) { 845 requestEnvelopeWriter.WriteObjectName("micro"); 846 requestEnvelopeWriter.WriteString(version.Micro()); 847 } 848 if (!version.PreRelease().IsEmpty()) { 849 requestEnvelopeWriter.WriteObjectName("preRelease"); 850 requestEnvelopeWriter.WriteString(version.PreRelease()); 851 } 852 if (version.Revision() != 0) { 853 requestEnvelopeWriter.WriteObjectName("revision"); 854 requestEnvelopeWriter.WriteInteger( 855 static_cast<int64>(version.Revision())); 856 } 857 858 requestEnvelopeWriter.WriteObjectEnd(); 859 860 return _SendJsonRequest("pkg/increment-view-counter", 861 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 862 0, message); 863 } 864 865 866 status_t 867 WebAppInterface::RetrievePasswordRequirements( 868 PasswordRequirements& passwordRequirements) 869 { 870 BMessage responseEnvelopeMessage; 871 status_t result = _RetrievePasswordRequirementsMeta( 872 responseEnvelopeMessage); 873 874 if (result != B_OK) 875 return result; 876 877 BMessage resultMessage; 878 879 result = responseEnvelopeMessage.FindMessage("result", &resultMessage); 880 881 if (result != B_OK) { 882 HDERROR("bad response envelope missing 'result' entry"); 883 return result; 884 } 885 886 double value; 887 888 if (resultMessage.FindDouble("minPasswordLength", &value) == B_OK) 889 passwordRequirements.SetMinPasswordLength((uint32) value); 890 891 if (resultMessage.FindDouble("minPasswordUppercaseChar", &value) == B_OK) 892 passwordRequirements.SetMinPasswordUppercaseChar((uint32) value); 893 894 if (resultMessage.FindDouble("minPasswordDigitsChar", &value) == B_OK) 895 passwordRequirements.SetMinPasswordDigitsChar((uint32) value); 896 897 return result; 898 } 899 900 901 status_t 902 WebAppInterface::_RetrievePasswordRequirementsMeta(BMessage& message) 903 { 904 BMallocIO* requestEnvelopeData = new BMallocIO(); 905 // BHttpRequest later takes ownership of this. 906 BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); 907 908 requestEnvelopeWriter.WriteObjectStart(); 909 requestEnvelopeWriter.WriteObjectEnd(); 910 911 return _SendJsonRequest("user/get-password-requirements", 912 requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), 913 0, message); 914 } 915 916 917 /*! JSON-RPC invocations return a response. The response may be either 918 a result or it may be an error depending on the response structure. 919 If it is an error then there may be additional detail that is the 920 error code and message. This method will extract the error code 921 from the response. This method will return 0 if the payload does 922 not look like an error. 923 */ 924 925 /*static*/ int32 926 WebAppInterface::ErrorCodeFromResponse(BMessage& responseEnvelopeMessage) 927 { 928 BMessage error; 929 double code; 930 931 if (responseEnvelopeMessage.FindMessage("error", &error) == B_OK 932 && error.FindDouble("code", &code) == B_OK) { 933 return (int32) code; 934 } 935 936 return 0; 937 } 938 939 940 // #pragma mark - private 941 942 943 status_t 944 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 945 BPositionIO* requestData, size_t requestDataSize, uint32 flags, 946 BMessage& reply) 947 { 948 bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; 949 AccessToken accessToken; 950 951 if (needsAuthorization) 952 accessToken = _ObtainValidAccessToken(); 953 954 return _SendJsonRequest(urlPathComponents, accessToken, requestData, 955 requestDataSize, flags, reply); 956 } 957 958 959 /*static*/ status_t 960 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 961 const AccessToken& accessToken, BPositionIO* requestData, 962 size_t requestDataSize, uint32 flags, BMessage& reply) 963 { 964 if (requestDataSize == 0) { 965 HDINFO("%s; empty request payload", PROTOCOL_NAME); 966 return B_ERROR; 967 } 968 969 if (!ServerHelper::IsNetworkAvailable()) { 970 HDDEBUG("%s; dropping request to ...[%s] as network is not" 971 " available", PROTOCOL_NAME, urlPathComponents); 972 delete requestData; 973 return HD_NETWORK_INACCESSIBLE; 974 } 975 976 if (ServerSettings::IsClientTooOld()) { 977 HDDEBUG("%s; dropping request to ...[%s] as client is too old", 978 PROTOCOL_NAME, urlPathComponents); 979 delete requestData; 980 return HD_CLIENT_TOO_OLD; 981 } 982 983 bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; 984 985 if (needsAuthorization && !accessToken.IsValid()) { 986 HDDEBUG("%s; dropping request to ...[%s] as access token is not valid", 987 PROTOCOL_NAME, urlPathComponents); 988 delete requestData; 989 return B_NOT_ALLOWED; 990 } 991 992 BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v2/") 993 << urlPathComponents); 994 HDDEBUG("%s; will make request to [%s]", PROTOCOL_NAME, 995 url.UrlString().String()); 996 997 // If the request payload is logged then it must be copied to local memory 998 // from the stream. This then requires that the request data is then 999 // delivered from memory. 1000 1001 if (Logger::IsTraceEnabled()) { 1002 HDLOGPREFIX(LOG_LEVEL_TRACE) 1003 printf("%s request; ", PROTOCOL_NAME); 1004 _LogPayload(requestData, requestDataSize); 1005 printf("\n"); 1006 } 1007 1008 ProtocolListener listener; 1009 BUrlContext context; 1010 1011 BHttpHeaders headers; 1012 headers.AddHeader("Content-Type", "application/json"); 1013 headers.AddHeader("Accept", "application/json"); 1014 ServerSettings::AugmentHeaders(headers); 1015 1016 BHttpRequest* request = make_http_request(url, NULL, &listener, &context); 1017 ObjectDeleter<BHttpRequest> _(request); 1018 if (request == NULL) 1019 return B_ERROR; 1020 request->SetMethod(B_HTTP_POST); 1021 request->SetHeaders(headers); 1022 1023 if (needsAuthorization) { 1024 BHttpAuthentication authentication; 1025 authentication.SetMethod(B_HTTP_AUTHENTICATION_BEARER); 1026 authentication.SetToken(accessToken.Token()); 1027 context.AddAuthentication(url, authentication); 1028 } 1029 1030 request->AdoptInputData(requestData, requestDataSize); 1031 1032 BMallocIO replyData; 1033 request->SetOutput(&replyData); 1034 1035 thread_id thread = request->Run(); 1036 wait_for_thread(thread, NULL); 1037 1038 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 1039 request->Result()); 1040 1041 int32 statusCode = result.StatusCode(); 1042 1043 HDDEBUG("%s; did receive http-status [%" B_PRId32 "] from [%s]", 1044 PROTOCOL_NAME, statusCode, url.UrlString().String()); 1045 1046 switch (statusCode) { 1047 case B_HTTP_STATUS_OK: 1048 break; 1049 1050 case B_HTTP_STATUS_PRECONDITION_FAILED: 1051 ServerHelper::NotifyClientTooOld(result.Headers()); 1052 return HD_CLIENT_TOO_OLD; 1053 1054 default: 1055 HDERROR("%s; request to endpoint [.../%s] failed with http " 1056 "status [%" B_PRId32 "]\n", PROTOCOL_NAME, urlPathComponents, 1057 statusCode); 1058 return B_ERROR; 1059 } 1060 1061 replyData.Seek(0, SEEK_SET); 1062 1063 if (Logger::IsTraceEnabled()) { 1064 HDLOGPREFIX(LOG_LEVEL_TRACE) 1065 printf("%s; response; ", PROTOCOL_NAME); 1066 _LogPayload(&replyData, replyData.BufferLength()); 1067 printf("\n"); 1068 } 1069 1070 BJsonMessageWriter jsonMessageWriter(reply); 1071 BJson::Parse(&replyData, &jsonMessageWriter); 1072 status_t status = jsonMessageWriter.ErrorStatus(); 1073 1074 if (Logger::IsTraceEnabled() && status == B_BAD_DATA) { 1075 BString resultString(static_cast<const char *>(replyData.Buffer()), 1076 replyData.BufferLength()); 1077 HDERROR("Parser choked on JSON:\n%s", resultString.String()); 1078 } 1079 return status; 1080 } 1081 1082 1083 status_t 1084 WebAppInterface::_SendJsonRequest(const char* urlPathComponents, 1085 const BString& jsonString, uint32 flags, BMessage& reply) 1086 { 1087 // gets 'adopted' by the subsequent http request. 1088 BMemoryIO* data = new BMemoryIO(jsonString.String(), 1089 jsonString.Length() - 1); 1090 1091 return _SendJsonRequest(urlPathComponents, data, jsonString.Length() - 1, 1092 flags, reply); 1093 } 1094 1095 1096 status_t 1097 WebAppInterface::_SendRawGetRequest(const BString urlPathComponents, 1098 BDataIO* stream) 1099 { 1100 BUrl url = ServerSettings::CreateFullUrl(urlPathComponents); 1101 1102 HDDEBUG("http-get; will make request to [%s]", 1103 url.UrlString().String()); 1104 1105 ProtocolListener listener; 1106 1107 BHttpHeaders headers; 1108 ServerSettings::AugmentHeaders(headers); 1109 1110 BHttpRequest *request = make_http_request(url, stream, &listener); 1111 ObjectDeleter<BHttpRequest> _(request); 1112 if (request == NULL) 1113 return B_ERROR; 1114 request->SetMethod(B_HTTP_GET); 1115 request->SetHeaders(headers); 1116 1117 thread_id thread = request->Run(); 1118 wait_for_thread(thread, NULL); 1119 1120 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 1121 request->Result()); 1122 1123 int32 statusCode = result.StatusCode(); 1124 1125 HDDEBUG("http-get; did receive http-status [%" B_PRId32 "] from [%s]", 1126 statusCode, url.UrlString().String()); 1127 1128 if (statusCode == 200) 1129 return B_OK; 1130 1131 HDERROR("failed to get data from '%s': %" B_PRIi32 "", 1132 url.UrlString().String(), statusCode); 1133 return B_ERROR; 1134 } 1135 1136 1137 void 1138 WebAppInterface::_LogPayload(BPositionIO* requestData, size_t size) 1139 { 1140 off_t requestDataOffset = requestData->Position(); 1141 char buffer[LOG_PAYLOAD_LIMIT]; 1142 1143 if (size > LOG_PAYLOAD_LIMIT) 1144 size = LOG_PAYLOAD_LIMIT; 1145 1146 if (B_OK != requestData->ReadExactly(buffer, size)) { 1147 printf("%s; error logging payload", PROTOCOL_NAME); 1148 } else { 1149 for (uint32 i = 0; i < size; i++) { 1150 bool esc = buffer[i] > 126 || 1151 (buffer[i] < 0x20 && buffer[i] != 0x0a); 1152 1153 if (esc) 1154 printf("\\u%02x", buffer[i]); 1155 else 1156 putchar(buffer[i]); 1157 } 1158 1159 if (size == LOG_PAYLOAD_LIMIT) 1160 printf("...(continues)"); 1161 } 1162 1163 requestData->Seek(requestDataOffset, SEEK_SET); 1164 } 1165 1166 1167 /*! This will get the position of the data to get the length an then sets the 1168 offset to zero so that it can be re-read for reading the payload in to log 1169 or send. 1170 */ 1171 1172 off_t 1173 WebAppInterface::_LengthAndSeekToZero(BPositionIO* data) 1174 { 1175 off_t dataSize = data->Position(); 1176 data->Seek(0, SEEK_SET); 1177 return dataSize; 1178 } 1179 1180 1181 UserCredentials 1182 WebAppInterface::_Credentials() 1183 { 1184 AutoLocker<BLocker> lock(&fLock); 1185 return fCredentials; 1186 } 1187 1188 1189 AccessToken 1190 WebAppInterface::_ObtainValidAccessToken() 1191 { 1192 AutoLocker<BLocker> lock(&fLock); 1193 1194 uint64 now = static_cast<uint64>(time(NULL)) * 1000; 1195 1196 if (!fAccessToken.IsValid(now)) { 1197 HDINFO("clearing cached access token as it is no longer valid"); 1198 fAccessToken.Clear(); 1199 } 1200 1201 if (!fAccessToken.IsValid()) { 1202 HDINFO("no cached access token present; will obtain a new one"); 1203 AuthenticateUserRetainingAccessToken(); 1204 } 1205 1206 return fAccessToken; 1207 } 1208