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