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