1 /* 2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2016-2017, 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 <stdio.h> 10 11 #include <AppFileInfo.h> 12 #include <Application.h> 13 #include <Autolock.h> 14 #include <File.h> 15 #include <HttpHeaders.h> 16 #include <HttpRequest.h> 17 #include <Json.h> 18 #include <Message.h> 19 #include <Roster.h> 20 #include <Url.h> 21 #include <UrlContext.h> 22 #include <UrlProtocolListener.h> 23 #include <UrlProtocolRoster.h> 24 25 #include "AutoLocker.h" 26 #include "List.h" 27 #include "Logger.h" 28 #include "PackageInfo.h" 29 #include "ServerSettings.h" 30 31 32 #define BASEURL_DEFAULT "https://depot.haiku-os.org" 33 #define USERAGENT_FALLBACK_VERSION "0.0.0" 34 35 36 class JsonBuilder { 37 public: 38 JsonBuilder() 39 : 40 fString("{"), 41 fInList(false) 42 { 43 } 44 45 JsonBuilder& AddObject() 46 { 47 fString << '{'; 48 fInList = false; 49 return *this; 50 } 51 52 JsonBuilder& AddObject(const char* name) 53 { 54 _StartName(name); 55 fString << '{'; 56 fInList = false; 57 return *this; 58 } 59 60 JsonBuilder& EndObject() 61 { 62 fString << '}'; 63 fInList = true; 64 return *this; 65 } 66 67 JsonBuilder& AddArray(const char* name) 68 { 69 _StartName(name); 70 fString << '['; 71 fInList = false; 72 return *this; 73 } 74 75 JsonBuilder& EndArray() 76 { 77 fString << ']'; 78 fInList = true; 79 return *this; 80 } 81 82 JsonBuilder& AddStrings(const StringList& strings) 83 { 84 for (int i = 0; i < strings.CountItems(); i++) 85 AddItem(strings.ItemAtFast(i)); 86 return *this; 87 } 88 89 JsonBuilder& AddItem(const char* item) 90 { 91 return AddItem(item, false); 92 } 93 94 JsonBuilder& AddItem(const char* item, bool nullIfEmpty) 95 { 96 if (item == NULL || (nullIfEmpty && strlen(item) == 0)) { 97 if (fInList) 98 fString << ",null"; 99 else 100 fString << "null"; 101 } else { 102 if (fInList) 103 fString << ",\""; 104 else 105 fString << '"'; 106 fString << _EscapeString(item); 107 fString << '"'; 108 } 109 fInList = true; 110 return *this; 111 } 112 113 JsonBuilder& AddValue(const char* name, const char* value) 114 { 115 return AddValue(name, value, false); 116 } 117 118 JsonBuilder& AddValue(const char* name, const char* value, 119 bool nullIfEmpty) 120 { 121 _StartName(name); 122 if (value == NULL || (nullIfEmpty && strlen(value) == 0)) { 123 fString << "null"; 124 } else { 125 fString << '"'; 126 fString << _EscapeString(value); 127 fString << '"'; 128 } 129 fInList = true; 130 return *this; 131 } 132 133 JsonBuilder& AddValue(const char* name, int value) 134 { 135 _StartName(name); 136 fString << value; 137 fInList = true; 138 return *this; 139 } 140 141 JsonBuilder& AddValue(const char* name, bool value) 142 { 143 _StartName(name); 144 if (value) 145 fString << "true"; 146 else 147 fString << "false"; 148 fInList = true; 149 return *this; 150 } 151 152 const BString& End() 153 { 154 fString << "}\n"; 155 return fString; 156 } 157 158 private: 159 void _StartName(const char* name) 160 { 161 if (fInList) 162 fString << ",\""; 163 else 164 fString << '"'; 165 fString << _EscapeString(name); 166 fString << "\":"; 167 } 168 169 BString _EscapeString(const char* original) const 170 { 171 BString string(original); 172 string.ReplaceAll("\\", "\\\\"); 173 string.ReplaceAll("\"", "\\\""); 174 string.ReplaceAll("/", "\\/"); 175 string.ReplaceAll("\b", "\\b"); 176 string.ReplaceAll("\f", "\\f"); 177 string.ReplaceAll("\n", "\\n"); 178 string.ReplaceAll("\r", "\\r"); 179 string.ReplaceAll("\t", "\\t"); 180 return string; 181 } 182 183 private: 184 BString fString; 185 bool fInList; 186 }; 187 188 189 class ProtocolListener : public BUrlProtocolListener { 190 public: 191 ProtocolListener(bool traceLogging) 192 : 193 fDownloadIO(NULL), 194 fTraceLogging(traceLogging) 195 { 196 } 197 198 virtual ~ProtocolListener() 199 { 200 } 201 202 virtual void ConnectionOpened(BUrlRequest* caller) 203 { 204 } 205 206 virtual void HostnameResolved(BUrlRequest* caller, const char* ip) 207 { 208 } 209 210 virtual void ResponseStarted(BUrlRequest* caller) 211 { 212 } 213 214 virtual void HeadersReceived(BUrlRequest* caller, const BUrlResult& result) 215 { 216 } 217 218 virtual void DataReceived(BUrlRequest* caller, const char* data, 219 off_t position, ssize_t size) 220 { 221 if (fDownloadIO != NULL) 222 fDownloadIO->Write(data, size); 223 } 224 225 virtual void DownloadProgress(BUrlRequest* caller, ssize_t bytesReceived, 226 ssize_t bytesTotal) 227 { 228 } 229 230 virtual void UploadProgress(BUrlRequest* caller, ssize_t bytesSent, 231 ssize_t bytesTotal) 232 { 233 } 234 235 virtual void RequestCompleted(BUrlRequest* caller, bool success) 236 { 237 } 238 239 virtual void DebugMessage(BUrlRequest* caller, 240 BUrlProtocolDebugMessage type, const char* text) 241 { 242 if (fTraceLogging) 243 printf("jrpc: %s\n", text); 244 } 245 246 void SetDownloadIO(BDataIO* downloadIO) 247 { 248 fDownloadIO = downloadIO; 249 } 250 251 private: 252 BDataIO* fDownloadIO; 253 bool fTraceLogging; 254 }; 255 256 257 int 258 WebAppInterface::fRequestIndex = 0; 259 260 261 enum { 262 NEEDS_AUTHORIZATION = 1 << 0, 263 }; 264 265 266 WebAppInterface::WebAppInterface() 267 : 268 fLanguage("en") 269 { 270 } 271 272 273 WebAppInterface::WebAppInterface(const WebAppInterface& other) 274 : 275 fUsername(other.fUsername), 276 fPassword(other.fPassword), 277 fLanguage(other.fLanguage) 278 { 279 } 280 281 282 WebAppInterface::~WebAppInterface() 283 { 284 } 285 286 287 WebAppInterface& 288 WebAppInterface::operator=(const WebAppInterface& other) 289 { 290 if (this == &other) 291 return *this; 292 293 fUsername = other.fUsername; 294 fPassword = other.fPassword; 295 fLanguage = other.fLanguage; 296 297 return *this; 298 } 299 300 301 void 302 WebAppInterface::SetAuthorization(const BString& username, 303 const BString& password) 304 { 305 fUsername = username; 306 fPassword = password; 307 } 308 309 310 void 311 WebAppInterface::SetPreferredLanguage(const BString& language) 312 { 313 fLanguage = language; 314 } 315 316 317 status_t 318 WebAppInterface::RetrieveRepositoriesForSourceBaseURLs( 319 const StringList& repositorySourceBaseURLs, 320 BMessage& message) 321 { 322 BString jsonString = JsonBuilder() 323 .AddValue("jsonrpc", "2.0") 324 .AddValue("id", ++fRequestIndex) 325 .AddValue("method", "searchRepositories") 326 .AddArray("params") 327 .AddObject() 328 .AddArray("repositorySourceSearchUrls") 329 .AddStrings(repositorySourceBaseURLs) 330 .EndArray() 331 .AddValue("offset", 0) 332 .AddValue("limit", 1000) // effectively a safety limit 333 .EndObject() 334 .EndArray() 335 .End(); 336 337 return _SendJsonRequest("repository", jsonString, 0, message); 338 } 339 340 341 status_t 342 WebAppInterface::RetrievePackageInfo(const BString& packageName, 343 const BString& architecture, const BString& repositoryCode, 344 BMessage& message) 345 { 346 BString jsonString = JsonBuilder() 347 .AddValue("jsonrpc", "2.0") 348 .AddValue("id", ++fRequestIndex) 349 .AddValue("method", "getPkg") 350 .AddArray("params") 351 .AddObject() 352 .AddValue("name", packageName) 353 .AddValue("architectureCode", architecture) 354 .AddValue("naturalLanguageCode", fLanguage) 355 .AddValue("repositoryCode", repositoryCode) 356 .AddValue("versionType", "NONE") 357 .EndObject() 358 .EndArray() 359 .End(); 360 361 return _SendJsonRequest("pkg", jsonString, 0, message); 362 } 363 364 365 status_t 366 WebAppInterface::RetrieveBulkPackageInfo(const StringList& packageNames, 367 const StringList& packageArchitectures, 368 const StringList& repositoryCodes, BMessage& message) 369 { 370 BString jsonString = JsonBuilder() 371 .AddValue("jsonrpc", "2.0") 372 .AddValue("id", ++fRequestIndex) 373 .AddValue("method", "getBulkPkg") 374 .AddArray("params") 375 .AddObject() 376 .AddArray("pkgNames") 377 .AddStrings(packageNames) 378 .EndArray() 379 .AddArray("architectureCodes") 380 .AddStrings(packageArchitectures) 381 .EndArray() 382 .AddArray("repositoryCodes") 383 .AddStrings(repositoryCodes) 384 .EndArray() 385 .AddValue("naturalLanguageCode", fLanguage) 386 .AddValue("versionType", "LATEST") 387 .AddArray("filter") 388 .AddItem("PKGCATEGORIES") 389 .AddItem("PKGSCREENSHOTS") 390 .AddItem("PKGVERSIONLOCALIZATIONDESCRIPTIONS") 391 .AddItem("PKGCHANGELOG") 392 .EndArray() 393 .EndObject() 394 .EndArray() 395 .End(); 396 397 return _SendJsonRequest("pkg", jsonString, 0, message); 398 } 399 400 401 status_t 402 WebAppInterface::RetrieveUserRatings(const BString& packageName, 403 const BString& architecture, int resultOffset, int maxResults, 404 BMessage& message) 405 { 406 BString jsonString = JsonBuilder() 407 .AddValue("jsonrpc", "2.0") 408 .AddValue("id", ++fRequestIndex) 409 .AddValue("method", "searchUserRatings") 410 .AddArray("params") 411 .AddObject() 412 .AddValue("pkgName", packageName) 413 .AddValue("pkgVersionArchitectureCode", architecture) 414 .AddValue("offset", resultOffset) 415 .AddValue("limit", maxResults) 416 .EndObject() 417 .EndArray() 418 .End(); 419 420 return _SendJsonRequest("userrating", jsonString, 0, message); 421 } 422 423 424 status_t 425 WebAppInterface::RetrieveUserRating(const BString& packageName, 426 const BPackageVersion& version, const BString& architecture, 427 const BString &repositoryCode, const BString& username, 428 BMessage& message) 429 { 430 BString jsonString = JsonBuilder() 431 .AddValue("jsonrpc", "2.0") 432 .AddValue("id", ++fRequestIndex) 433 .AddValue("method", "getUserRatingByUserAndPkgVersion") 434 .AddArray("params") 435 .AddObject() 436 .AddValue("userNickname", username) 437 .AddValue("pkgName", packageName) 438 .AddValue("pkgVersionArchitectureCode", architecture) 439 .AddValue("pkgVersionMajor", version.Major(), true) 440 .AddValue("pkgVersionMinor", version.Minor(), true) 441 .AddValue("pkgVersionMicro", version.Micro(), true) 442 .AddValue("pkgVersionPreRelease", version.PreRelease(), true) 443 .AddValue("pkgVersionRevision", (int)version.Revision()) 444 .AddValue("repositoryCode", repositoryCode) 445 .EndObject() 446 .EndArray() 447 .End(); 448 449 return _SendJsonRequest("userrating", jsonString, 0, message); 450 } 451 452 453 status_t 454 WebAppInterface::CreateUserRating(const BString& packageName, 455 const BString& architecture, const BString& repositoryCode, 456 const BString& languageCode, const BString& comment, 457 const BString& stability, int rating, BMessage& message) 458 { 459 BString jsonString = JsonBuilder() 460 .AddValue("jsonrpc", "2.0") 461 .AddValue("id", ++fRequestIndex) 462 .AddValue("method", "createUserRating") 463 .AddArray("params") 464 .AddObject() 465 .AddValue("pkgName", packageName) 466 .AddValue("pkgVersionArchitectureCode", architecture) 467 .AddValue("pkgVersionType", "LATEST") 468 .AddValue("userNickname", fUsername) 469 .AddValue("rating", rating) 470 .AddValue("userRatingStabilityCode", stability, true) 471 .AddValue("comment", comment) 472 .AddValue("repositoryCode", repositoryCode) 473 .AddValue("naturalLanguageCode", languageCode) 474 .EndObject() 475 .EndArray() 476 .End(); 477 478 return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION, 479 message); 480 } 481 482 483 status_t 484 WebAppInterface::UpdateUserRating(const BString& ratingID, 485 const BString& languageCode, const BString& comment, 486 const BString& stability, int rating, bool active, BMessage& message) 487 { 488 BString jsonString = JsonBuilder() 489 .AddValue("jsonrpc", "2.0") 490 .AddValue("id", ++fRequestIndex) 491 .AddValue("method", "updateUserRating") 492 .AddArray("params") 493 .AddObject() 494 .AddValue("code", ratingID) 495 .AddValue("rating", rating) 496 .AddValue("userRatingStabilityCode", stability, true) 497 .AddValue("comment", comment) 498 .AddValue("naturalLanguageCode", languageCode) 499 .AddValue("active", active) 500 .AddArray("filter") 501 .AddItem("ACTIVE") 502 .AddItem("NATURALLANGUAGE") 503 .AddItem("USERRATINGSTABILITY") 504 .AddItem("COMMENT") 505 .AddItem("RATING") 506 .EndArray() 507 .EndObject() 508 .EndArray() 509 .End(); 510 511 return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION, 512 message); 513 } 514 515 516 status_t 517 WebAppInterface::RetrieveScreenshot(const BString& code, 518 int32 width, int32 height, BDataIO* stream) 519 { 520 BUrl url = ServerSettings::CreateFullUrl( 521 BString("/__pkgscreenshot/") << code << ".png" << "?tw=" 522 << width << "&th=" << height); 523 524 bool isSecure = url.Protocol() == "https"; 525 526 ProtocolListener listener(Logger::IsTraceEnabled()); 527 listener.SetDownloadIO(stream); 528 529 BHttpHeaders headers; 530 ServerSettings::AugmentHeaders(headers); 531 532 BHttpRequest request(url, isSecure, "HTTP", &listener); 533 request.SetMethod(B_HTTP_GET); 534 request.SetHeaders(headers); 535 536 thread_id thread = request.Run(); 537 wait_for_thread(thread, NULL); 538 539 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 540 request.Result()); 541 542 int32 statusCode = result.StatusCode(); 543 544 if (statusCode == 200) 545 return B_OK; 546 547 fprintf(stderr, "failed to get screenshot from '%s': %" B_PRIi32 "\n", 548 url.UrlString().String(), statusCode); 549 return B_ERROR; 550 } 551 552 553 status_t 554 WebAppInterface::RequestCaptcha(BMessage& message) 555 { 556 BString jsonString = JsonBuilder() 557 .AddValue("jsonrpc", "2.0") 558 .AddValue("id", ++fRequestIndex) 559 .AddValue("method", "generateCaptcha") 560 .AddArray("params") 561 .AddObject() 562 .EndObject() 563 .EndArray() 564 .End(); 565 566 return _SendJsonRequest("captcha", jsonString, 0, message); 567 } 568 569 570 status_t 571 WebAppInterface::CreateUser(const BString& nickName, 572 const BString& passwordClear, const BString& email, 573 const BString& captchaToken, const BString& captchaResponse, 574 const BString& languageCode, BMessage& message) 575 { 576 JsonBuilder builder; 577 builder 578 .AddValue("jsonrpc", "2.0") 579 .AddValue("id", ++fRequestIndex) 580 .AddValue("method", "createUser") 581 .AddArray("params") 582 .AddObject() 583 .AddValue("nickname", nickName) 584 .AddValue("passwordClear", passwordClear); 585 586 if (!email.IsEmpty()) 587 builder.AddValue("email", email); 588 589 builder.AddValue("captchaToken", captchaToken) 590 .AddValue("captchaResponse", captchaResponse) 591 .AddValue("naturalLanguageCode", languageCode) 592 .EndObject() 593 .EndArray() 594 ; 595 596 BString jsonString = builder.End(); 597 598 return _SendJsonRequest("user", jsonString, 0, message); 599 } 600 601 602 status_t 603 WebAppInterface::AuthenticateUser(const BString& nickName, 604 const BString& passwordClear, BMessage& message) 605 { 606 BString jsonString = JsonBuilder() 607 .AddValue("jsonrpc", "2.0") 608 .AddValue("id", ++fRequestIndex) 609 .AddValue("method", "authenticateUser") 610 .AddArray("params") 611 .AddObject() 612 .AddValue("nickname", nickName) 613 .AddValue("passwordClear", passwordClear) 614 .EndObject() 615 .EndArray() 616 .End(); 617 618 return _SendJsonRequest("user", jsonString, 0, message); 619 } 620 621 622 // #pragma mark - private 623 624 625 status_t 626 WebAppInterface::_SendJsonRequest(const char* domain, BString jsonString, 627 uint32 flags, BMessage& reply) const 628 { 629 if (Logger::IsTraceEnabled()) 630 printf("_SendJsonRequest(%s)\n", jsonString.String()); 631 632 BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v1/") << domain); 633 bool isSecure = url.Protocol() == "https"; 634 635 ProtocolListener listener(Logger::IsTraceEnabled()); 636 BUrlContext context; 637 638 BHttpHeaders headers; 639 headers.AddHeader("Content-Type", "application/json"); 640 ServerSettings::AugmentHeaders(headers); 641 642 BHttpRequest request(url, isSecure, "HTTP", &listener, &context); 643 request.SetMethod(B_HTTP_POST); 644 request.SetHeaders(headers); 645 646 // Authentication via Basic Authentication 647 // The other way would be to obtain a token and then use the Token Bearer 648 // header. 649 if ((flags & NEEDS_AUTHORIZATION) != 0 650 && !fUsername.IsEmpty() && !fPassword.IsEmpty()) { 651 BHttpAuthentication authentication(fUsername, fPassword); 652 authentication.SetMethod(B_HTTP_AUTHENTICATION_BASIC); 653 context.AddAuthentication(url, authentication); 654 } 655 656 BMemoryIO* data = new BMemoryIO( 657 jsonString.String(), jsonString.Length() - 1); 658 659 request.AdoptInputData(data, jsonString.Length() - 1); 660 661 BMallocIO replyData; 662 listener.SetDownloadIO(&replyData); 663 664 thread_id thread = request.Run(); 665 wait_for_thread(thread, NULL); 666 667 const BHttpResult& result = dynamic_cast<const BHttpResult&>( 668 request.Result()); 669 670 int32 statusCode = result.StatusCode(); 671 if (statusCode != 200) { 672 printf("Response code: %" B_PRId32 "\n", statusCode); 673 return B_ERROR; 674 } 675 676 jsonString.SetTo(static_cast<const char*>(replyData.Buffer()), 677 replyData.BufferLength()); 678 if (jsonString.Length() == 0) 679 return B_ERROR; 680 681 status_t status = BJson::Parse(jsonString, reply); 682 if (Logger::IsTraceEnabled() && status == B_BAD_DATA) { 683 printf("Parser choked on JSON:\n%s\n", jsonString.String()); 684 } 685 return status; 686 } 687