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