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:
ProtocolListener()42 ProtocolListener()
43 {
44 }
45
~ProtocolListener()46 virtual ~ProtocolListener()
47 {
48 }
49
ConnectionOpened(BUrlRequest * caller)50 virtual void ConnectionOpened(BUrlRequest* caller)
51 {
52 }
53
HostnameResolved(BUrlRequest * caller,const char * ip)54 virtual void HostnameResolved(BUrlRequest* caller, const char* ip)
55 {
56 }
57
ResponseStarted(BUrlRequest * caller)58 virtual void ResponseStarted(BUrlRequest* caller)
59 {
60 }
61
HeadersReceived(BUrlRequest * caller)62 virtual void HeadersReceived(BUrlRequest* caller)
63 {
64 }
65
BytesWritten(BUrlRequest * caller,size_t bytesWritten)66 virtual void BytesWritten(BUrlRequest* caller, size_t bytesWritten)
67 {
68 }
69
DownloadProgress(BUrlRequest * caller,off_t bytesReceived,off_t bytesTotal)70 virtual void DownloadProgress(BUrlRequest* caller, off_t bytesReceived, off_t bytesTotal)
71 {
72 }
73
UploadProgress(BUrlRequest * caller,off_t bytesSent,off_t bytesTotal)74 virtual void UploadProgress(BUrlRequest* caller, off_t bytesSent, off_t bytesTotal)
75 {
76 }
77
RequestCompleted(BUrlRequest * caller,bool success)78 virtual void RequestCompleted(BUrlRequest* caller, bool success)
79 {
80 }
81
DebugMessage(BUrlRequest * caller,BUrlProtocolDebugMessage type,const char * text)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*
make_http_request(const BUrl & url,BDataIO * output,BUrlProtocolListener * listener=NULL,BUrlContext * context=NULL)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
WebAppInterface()111 WebAppInterface::WebAppInterface()
112 {
113 }
114
115
~WebAppInterface()116 WebAppInterface::~WebAppInterface()
117 {
118 }
119
120
121 void
SetCredentials(const UserCredentials & value)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&
Nickname()133 WebAppInterface::Nickname()
134 {
135 AutoLocker<BLocker> lock(&fLock);
136 return fCredentials.Nickname();
137 }
138
139
140 status_t
GetChangelog(const BString & packageName,BMessage & message)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
RetrieveUserRatingSummaryForPackage(const BString & packageName,const BString & webAppRepositoryCode,BMessage & message)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
RetrieveUserRatingsForPackageForDisplay(const BString & packageName,const BString & webAppRepositoryCode,const BString & webAppRepositorySourceCode,int resultOffset,int maxResults,BMessage & message)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
RetrieveUserRatingForPackageAndVersionByUser(const BString & packageName,const BPackageVersion & version,const BString & architecture,const BString & webAppRepositoryCode,const BString & webAppRepositorySourceCode,const BString & userNickname,BMessage & message)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
RetrieveUserDetailForCredentials(const UserCredentials & credentials,BMessage & message)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
RetrieveCurrentUserDetail(BMessage & message)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
UnpackUserDetail(BMessage & responseEnvelopeMessage,UserDetail & userDetail)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
UnpackAccessToken(BMessage & responseEnvelopeMessage,AccessToken & accessToken)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
RetrieveUserUsageConditions(const BString & code,UserUsageConditions & conditions)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
AgreeUserUsageConditions(const BString & code,BMessage & responsePayload)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
_RetrieveUserUsageConditionsMeta(const BString & code,BMessage & message)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
_RetrieveUserUsageConditionsCopy(const BString & code,BDataIO * stream)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
CreateUserRating(const BString & packageName,const BPackageVersion & version,const BString & architecture,const BString & webAppRepositoryCode,const BString & webAppRepositorySourceCode,const BString & naturalLanguageCode,const BString & comment,const BString & stability,int rating,BMessage & message)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
UpdateUserRating(const BString & ratingID,const BString & naturalLanguageCode,const BString & comment,const BString & stability,int rating,bool active,BMessage & message)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
RetrieveScreenshot(const BString & code,int32 width,int32 height,BDataIO * stream)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
RequestCaptcha(BMessage & message)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
CreateUser(const BString & nickName,const BString & passwordClear,const BString & email,const BString & captchaToken,const BString & captchaResponse,const BString & naturalLanguageCode,const BString & userUsageConditionsCode,BMessage & message)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
AuthenticateUserRetainingAccessToken()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
_AuthenticateUserRetainingAccessToken(const BString & nickName,const BString & passwordClear)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
AuthenticateUser(const BString & nickName,const BString & passwordClear,BMessage & message)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
IncrementViewCounter(const PackageInfoRef package,const DepotInfoRef depot,BMessage & message)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
RetrievePasswordRequirements(PasswordRequirements & passwordRequirements)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
_RetrievePasswordRequirementsMeta(BMessage & message)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
ErrorCodeFromResponse(BMessage & responseEnvelopeMessage)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
_SendJsonRequest(const char * urlPathComponents,BPositionIO * requestData,size_t requestDataSize,uint32 flags,BMessage & reply)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
_SendJsonRequest(const char * urlPathComponents,const AccessToken & accessToken,BPositionIO * requestData,size_t requestDataSize,uint32 flags,BMessage & reply)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
_SendJsonRequest(const char * urlPathComponents,const BString & jsonString,uint32 flags,BMessage & reply)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
_SendRawGetRequest(const BString urlPathComponents,BDataIO * stream)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
_LogPayload(BPositionIO * requestData,size_t size)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
_LengthAndSeekToZero(BPositionIO * data)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
_Credentials()1207 WebAppInterface::_Credentials()
1208 {
1209 AutoLocker<BLocker> lock(&fLock);
1210 return fCredentials;
1211 }
1212
1213
1214 AccessToken
_ObtainValidAccessToken()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