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