xref: /haiku/src/apps/haikudepot/server/WebAppInterface.cpp (revision 9f3bdf3d039430b5172c424def20ce5d9f7367d4)
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:
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& naturalLanguageCode,
553 		// This is the "ID" in the ICU system; the term `code` is used with the
554 		// server system.
555 	const BString& comment,
556 	const BString& stability, int rating, BMessage& message)
557 {
558 	BMallocIO* requestEnvelopeData = new BMallocIO();
559 		// BHttpRequest later takes ownership of this.
560 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
561 
562 	requestEnvelopeWriter.WriteObjectStart();
563 	requestEnvelopeWriter.WriteObjectName("pkgName");
564 	requestEnvelopeWriter.WriteString(packageName.String());
565 	requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode");
566 	requestEnvelopeWriter.WriteString(architecture.String());
567 	requestEnvelopeWriter.WriteObjectName("repositoryCode");
568 	requestEnvelopeWriter.WriteString(webAppRepositoryCode.String());
569 	requestEnvelopeWriter.WriteObjectName("repositorySourceCode");
570 	requestEnvelopeWriter.WriteString(webAppRepositorySourceCode.String());
571 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
572 	requestEnvelopeWriter.WriteString(naturalLanguageCode.String());
573 	requestEnvelopeWriter.WriteObjectName("pkgVersionType");
574 	requestEnvelopeWriter.WriteString("SPECIFIC");
575 	requestEnvelopeWriter.WriteObjectName("userNickname");
576 	requestEnvelopeWriter.WriteString(Nickname());
577 
578 	if (!version.Major().IsEmpty()) {
579 		requestEnvelopeWriter.WriteObjectName("pkgVersionMajor");
580 		requestEnvelopeWriter.WriteString(version.Major());
581 	}
582 
583 	if (!version.Minor().IsEmpty()) {
584 		requestEnvelopeWriter.WriteObjectName("pkgVersionMinor");
585 		requestEnvelopeWriter.WriteString(version.Minor());
586 	}
587 
588 	if (!version.Micro().IsEmpty()) {
589 		requestEnvelopeWriter.WriteObjectName("pkgVersionMicro");
590 		requestEnvelopeWriter.WriteString(version.Micro());
591 	}
592 
593 	if (!version.PreRelease().IsEmpty()) {
594 		requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease");
595 		requestEnvelopeWriter.WriteString(version.PreRelease());
596 	}
597 
598 	if (version.Revision() != 0) {
599 		requestEnvelopeWriter.WriteObjectName("pkgVersionRevision");
600 		requestEnvelopeWriter.WriteInteger(version.Revision());
601 	}
602 
603 	if (rating > 0.0f) {
604 		requestEnvelopeWriter.WriteObjectName("rating");
605     	requestEnvelopeWriter.WriteInteger(rating);
606 	}
607 
608 	if (stability.Length() > 0) {
609 		requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode");
610 		requestEnvelopeWriter.WriteString(stability);
611 	}
612 
613 	if (comment.Length() > 0) {
614 		requestEnvelopeWriter.WriteObjectName("comment");
615 		requestEnvelopeWriter.WriteString(comment.String());
616 	}
617 
618 	requestEnvelopeWriter.WriteObjectEnd();
619 
620 	return _SendJsonRequest("user-rating/create-user-rating",
621 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
622 		NEEDS_AUTHORIZATION, message);
623 }
624 
625 
626 status_t
627 WebAppInterface::UpdateUserRating(const BString& ratingID,
628 	const BString& naturalLanguageCode,
629 		// This is the "ID" in the ICU system; the term `code` is used with the
630 		// server system.
631 	const BString& comment,
632 	const BString& stability, int rating, bool active, BMessage& message)
633 {
634 	BMallocIO* requestEnvelopeData = new BMallocIO();
635 		// BHttpRequest later takes ownership of this.
636 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
637 
638 	requestEnvelopeWriter.WriteObjectStart();
639 
640 	requestEnvelopeWriter.WriteObjectName("code");
641 	requestEnvelopeWriter.WriteString(ratingID.String());
642 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
643 	requestEnvelopeWriter.WriteString(naturalLanguageCode.String());
644 	requestEnvelopeWriter.WriteObjectName("active");
645 	requestEnvelopeWriter.WriteBoolean(active);
646 
647 	requestEnvelopeWriter.WriteObjectName("filter");
648 	requestEnvelopeWriter.WriteArrayStart();
649 	requestEnvelopeWriter.WriteString("ACTIVE");
650 	requestEnvelopeWriter.WriteString("NATURALLANGUAGE");
651 	requestEnvelopeWriter.WriteString("USERRATINGSTABILITY");
652 	requestEnvelopeWriter.WriteString("COMMENT");
653 	requestEnvelopeWriter.WriteString("RATING");
654 	requestEnvelopeWriter.WriteArrayEnd();
655 
656 	if (rating >= 0) {
657 		requestEnvelopeWriter.WriteObjectName("rating");
658 		requestEnvelopeWriter.WriteInteger(rating);
659 	}
660 
661 	if (stability.Length() > 0) {
662 		requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode");
663 		requestEnvelopeWriter.WriteString(stability);
664 	}
665 
666 	if (comment.Length() > 0) {
667 		requestEnvelopeWriter.WriteObjectName("comment");
668 		requestEnvelopeWriter.WriteString(comment);
669 	}
670 
671 	requestEnvelopeWriter.WriteObjectEnd();
672 
673 	return _SendJsonRequest("user-rating/update-user-rating",
674 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
675 		NEEDS_AUTHORIZATION, message);
676 }
677 
678 
679 /*! This method will call to the server to get a screenshot that will fit into
680     the specified width and height.
681 */
682 
683 status_t
684 WebAppInterface::RetrieveScreenshot(const BString& code,
685 	int32 width, int32 height, BDataIO* stream)
686 {
687 	return _SendRawGetRequest(
688 		BString("/__pkgscreenshot/") << code << ".png" << "?tw="
689 			<< width << "&th=" << height, stream);
690 }
691 
692 
693 status_t
694 WebAppInterface::RequestCaptcha(BMessage& message)
695 {
696 	BMallocIO* requestEnvelopeData = new BMallocIO();
697 		// BHttpRequest later takes ownership of this.
698 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
699 
700 	requestEnvelopeWriter.WriteObjectStart();
701 	requestEnvelopeWriter.WriteObjectEnd();
702 
703 	return _SendJsonRequest("captcha/generate-captcha",
704 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
705 		0, message);
706 }
707 
708 
709 status_t
710 WebAppInterface::CreateUser(const BString& nickName,
711 	const BString& passwordClear,
712 	const BString& email,
713 	const BString& captchaToken,
714 	const BString& captchaResponse,
715 	const BString& naturalLanguageCode,
716 		// This is the "ID" in the ICU system; the term `code` is used with the
717 		// server system.
718 	const BString& userUsageConditionsCode,
719 	BMessage& message)
720 {
721 		// BHttpRequest later takes ownership of this.
722 	BMallocIO* requestEnvelopeData = new BMallocIO();
723 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
724 
725 	requestEnvelopeWriter.WriteObjectStart();
726 
727 	requestEnvelopeWriter.WriteObjectName("nickname");
728 	requestEnvelopeWriter.WriteString(nickName.String());
729 	requestEnvelopeWriter.WriteObjectName("passwordClear");
730 	requestEnvelopeWriter.WriteString(passwordClear.String());
731 	requestEnvelopeWriter.WriteObjectName("captchaToken");
732 	requestEnvelopeWriter.WriteString(captchaToken.String());
733 	requestEnvelopeWriter.WriteObjectName("captchaResponse");
734 	requestEnvelopeWriter.WriteString(captchaResponse.String());
735 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
736 	requestEnvelopeWriter.WriteString(naturalLanguageCode.String());
737 	requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode");
738 	requestEnvelopeWriter.WriteString(userUsageConditionsCode.String());
739 
740 	if (!email.IsEmpty()) {
741 		requestEnvelopeWriter.WriteObjectName("email");
742 		requestEnvelopeWriter.WriteString(email.String());
743 	}
744 
745 	requestEnvelopeWriter.WriteObjectEnd();
746 
747 	return _SendJsonRequest("user/create-user", requestEnvelopeData,
748 		_LengthAndSeekToZero(requestEnvelopeData), 0, message);
749 }
750 
751 
752 /*! This method will authenticate the user set in the credentials and will
753     retain the resultant access token for authenticating any latter API calls.
754 */
755 
756 status_t
757 WebAppInterface::AuthenticateUserRetainingAccessToken()
758 {
759 	UserCredentials userCredentials = _Credentials();
760 
761 	if (!userCredentials.IsValid()) {
762 		HDINFO("unable to get a new access token as there are no credentials");
763 		return B_NOT_INITIALIZED;
764 	}
765 
766 	return _AuthenticateUserRetainingAccessToken(userCredentials.Nickname(),
767 		userCredentials.PasswordClear());
768 }
769 
770 
771 status_t
772 WebAppInterface::_AuthenticateUserRetainingAccessToken(const BString& nickName,
773 	const BString& passwordClear) {
774 	AutoLocker<BLocker> lock(&fLock);
775 
776 	fAccessToken.Clear();
777 
778 	BMessage responseEnvelopeMessage;
779 	status_t result = AuthenticateUser(nickName, passwordClear, responseEnvelopeMessage);
780 
781 	AccessToken accessToken;
782 
783 	if (result == B_OK)
784 		result = UnpackAccessToken(responseEnvelopeMessage, accessToken);
785 
786 	if (result == B_OK)
787 		fAccessToken = accessToken;
788 
789 	return result;
790 }
791 
792 
793 status_t
794 WebAppInterface::AuthenticateUser(const BString& nickName,
795 	const BString& passwordClear, BMessage& message)
796 {
797 	BMallocIO* requestEnvelopeData = new BMallocIO();
798 		// BHttpRequest later takes ownership of this.
799 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
800 
801 	requestEnvelopeWriter.WriteObjectStart();
802 
803 	requestEnvelopeWriter.WriteObjectName("nickname");
804 	requestEnvelopeWriter.WriteString(nickName.String());
805 	requestEnvelopeWriter.WriteObjectName("passwordClear");
806 	requestEnvelopeWriter.WriteString(passwordClear.String());
807 
808 	requestEnvelopeWriter.WriteObjectEnd();
809 
810 	return _SendJsonRequest("user/authenticate-user",
811 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
812 		0, message);
813 }
814 
815 
816 status_t
817 WebAppInterface::IncrementViewCounter(const PackageInfoRef package,
818 	const DepotInfoRef depot, BMessage& message)
819 {
820 	BMallocIO* requestEnvelopeData = new BMallocIO();
821 		// BHttpRequest later takes ownership of this.
822 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
823 
824 	requestEnvelopeWriter.WriteObjectStart();
825 
826 	requestEnvelopeWriter.WriteObjectName("architectureCode");
827 	requestEnvelopeWriter.WriteString(package->Architecture());
828 	requestEnvelopeWriter.WriteObjectName("repositoryCode");
829 	requestEnvelopeWriter.WriteString(depot->WebAppRepositoryCode());
830 	requestEnvelopeWriter.WriteObjectName("repositorySourceCode");
831 	requestEnvelopeWriter.WriteString(depot->WebAppRepositorySourceCode());
832 	requestEnvelopeWriter.WriteObjectName("name");
833 	requestEnvelopeWriter.WriteString(package->Name());
834 
835 	const BPackageVersion version = package->Version();
836 	if (!version.Major().IsEmpty()) {
837 		requestEnvelopeWriter.WriteObjectName("major");
838 		requestEnvelopeWriter.WriteString(version.Major());
839 	}
840 	if (!version.Minor().IsEmpty()) {
841 		requestEnvelopeWriter.WriteObjectName("minor");
842 		requestEnvelopeWriter.WriteString(version.Minor());
843 	}
844 	if (!version.Micro().IsEmpty()) {
845 		requestEnvelopeWriter.WriteObjectName("micro");
846 		requestEnvelopeWriter.WriteString(version.Micro());
847 	}
848 	if (!version.PreRelease().IsEmpty()) {
849 		requestEnvelopeWriter.WriteObjectName("preRelease");
850 		requestEnvelopeWriter.WriteString(version.PreRelease());
851 	}
852 	if (version.Revision() != 0) {
853 		requestEnvelopeWriter.WriteObjectName("revision");
854 		requestEnvelopeWriter.WriteInteger(
855 			static_cast<int64>(version.Revision()));
856 	}
857 
858 	requestEnvelopeWriter.WriteObjectEnd();
859 
860 	return _SendJsonRequest("pkg/increment-view-counter",
861 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
862 		0, message);
863 }
864 
865 
866 status_t
867 WebAppInterface::RetrievePasswordRequirements(
868 	PasswordRequirements& passwordRequirements)
869 {
870 	BMessage responseEnvelopeMessage;
871 	status_t result = _RetrievePasswordRequirementsMeta(
872 		responseEnvelopeMessage);
873 
874 	if (result != B_OK)
875 		return result;
876 
877 	BMessage resultMessage;
878 
879 	result = responseEnvelopeMessage.FindMessage("result", &resultMessage);
880 
881 	if (result != B_OK) {
882 		HDERROR("bad response envelope missing 'result' entry");
883 		return result;
884 	}
885 
886 	double value;
887 
888 	if (resultMessage.FindDouble("minPasswordLength", &value) == B_OK)
889 		passwordRequirements.SetMinPasswordLength((uint32) value);
890 
891 	if (resultMessage.FindDouble("minPasswordUppercaseChar", &value) == B_OK)
892 		passwordRequirements.SetMinPasswordUppercaseChar((uint32) value);
893 
894 	if (resultMessage.FindDouble("minPasswordDigitsChar", &value) == B_OK)
895 		passwordRequirements.SetMinPasswordDigitsChar((uint32) value);
896 
897 	return result;
898 }
899 
900 
901 status_t
902 WebAppInterface::_RetrievePasswordRequirementsMeta(BMessage& message)
903 {
904 	BMallocIO* requestEnvelopeData = new BMallocIO();
905 		// BHttpRequest later takes ownership of this.
906 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
907 
908 	requestEnvelopeWriter.WriteObjectStart();
909 	requestEnvelopeWriter.WriteObjectEnd();
910 
911 	return _SendJsonRequest("user/get-password-requirements",
912 		requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData),
913 		0, message);
914 }
915 
916 
917 /*!	JSON-RPC invocations return a response.  The response may be either
918 	a result or it may be an error depending on the response structure.
919 	If it is an error then there may be additional detail that is the
920 	error code and message.  This method will extract the error code
921 	from the response.  This method will return 0 if the payload does
922 	not look like an error.
923 */
924 
925 /*static*/ int32
926 WebAppInterface::ErrorCodeFromResponse(BMessage& responseEnvelopeMessage)
927 {
928 	BMessage error;
929 	double code;
930 
931 	if (responseEnvelopeMessage.FindMessage("error", &error) == B_OK
932 		&& error.FindDouble("code", &code) == B_OK) {
933 		return (int32) code;
934 	}
935 
936 	return 0;
937 }
938 
939 
940 // #pragma mark - private
941 
942 
943 status_t
944 WebAppInterface::_SendJsonRequest(const char* urlPathComponents,
945 	BPositionIO* requestData, size_t requestDataSize, uint32 flags,
946 	BMessage& reply)
947 {
948 	bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0;
949 	AccessToken accessToken;
950 
951 	if (needsAuthorization)
952 		accessToken = _ObtainValidAccessToken();
953 
954 	return _SendJsonRequest(urlPathComponents, accessToken, requestData,
955 		requestDataSize, flags, reply);
956 }
957 
958 
959 /*static*/ status_t
960 WebAppInterface::_SendJsonRequest(const char* urlPathComponents,
961 	const AccessToken& accessToken, BPositionIO* requestData,
962 	size_t requestDataSize, uint32 flags, BMessage& reply)
963 {
964 	if (requestDataSize == 0) {
965 		HDINFO("%s; empty request payload", PROTOCOL_NAME);
966 		return B_ERROR;
967 	}
968 
969 	if (!ServerHelper::IsNetworkAvailable()) {
970 		HDDEBUG("%s; dropping request to ...[%s] as network is not"
971 			" available", PROTOCOL_NAME, urlPathComponents);
972 		delete requestData;
973 		return HD_NETWORK_INACCESSIBLE;
974 	}
975 
976 	if (ServerSettings::IsClientTooOld()) {
977 		HDDEBUG("%s; dropping request to ...[%s] as client is too old",
978 			PROTOCOL_NAME, urlPathComponents);
979 		delete requestData;
980 		return HD_CLIENT_TOO_OLD;
981 	}
982 
983 	bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0;
984 
985 	if (needsAuthorization && !accessToken.IsValid()) {
986 		HDDEBUG("%s; dropping request to ...[%s] as access token is not valid",
987 			PROTOCOL_NAME, urlPathComponents);
988 		delete requestData;
989 		return B_NOT_ALLOWED;
990 	}
991 
992 	BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v2/")
993 		<< urlPathComponents);
994 	HDDEBUG("%s; will make request to [%s]", PROTOCOL_NAME,
995 		url.UrlString().String());
996 
997 	// If the request payload is logged then it must be copied to local memory
998 	// from the stream.  This then requires that the request data is then
999 	// delivered from memory.
1000 
1001 	if (Logger::IsTraceEnabled()) {
1002 		HDLOGPREFIX(LOG_LEVEL_TRACE)
1003 		printf("%s request; ", PROTOCOL_NAME);
1004 		_LogPayload(requestData, requestDataSize);
1005 		printf("\n");
1006 	}
1007 
1008 	ProtocolListener listener;
1009 	BUrlContext context;
1010 
1011 	BHttpHeaders headers;
1012 	headers.AddHeader("Content-Type", "application/json");
1013 	headers.AddHeader("Accept", "application/json");
1014 	ServerSettings::AugmentHeaders(headers);
1015 
1016 	BHttpRequest* request = make_http_request(url, NULL, &listener, &context);
1017 	ObjectDeleter<BHttpRequest> _(request);
1018 	if (request == NULL)
1019 		return B_ERROR;
1020 	request->SetMethod(B_HTTP_POST);
1021 	request->SetHeaders(headers);
1022 
1023 	if (needsAuthorization) {
1024 		BHttpAuthentication authentication;
1025 		authentication.SetMethod(B_HTTP_AUTHENTICATION_BEARER);
1026 		authentication.SetToken(accessToken.Token());
1027 		context.AddAuthentication(url, authentication);
1028 	}
1029 
1030 	request->AdoptInputData(requestData, requestDataSize);
1031 
1032 	BMallocIO replyData;
1033 	request->SetOutput(&replyData);
1034 
1035 	thread_id thread = request->Run();
1036 	wait_for_thread(thread, NULL);
1037 
1038 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
1039 		request->Result());
1040 
1041 	int32 statusCode = result.StatusCode();
1042 
1043 	HDDEBUG("%s; did receive http-status [%" B_PRId32 "] from [%s]",
1044 		PROTOCOL_NAME, statusCode, url.UrlString().String());
1045 
1046 	switch (statusCode) {
1047 		case B_HTTP_STATUS_OK:
1048 			break;
1049 
1050 		case B_HTTP_STATUS_PRECONDITION_FAILED:
1051 			ServerHelper::NotifyClientTooOld(result.Headers());
1052 			return HD_CLIENT_TOO_OLD;
1053 
1054 		default:
1055 			HDERROR("%s; request to endpoint [.../%s] failed with http "
1056 				"status [%" B_PRId32 "]\n", PROTOCOL_NAME, urlPathComponents,
1057 				statusCode);
1058 			return B_ERROR;
1059 	}
1060 
1061 	replyData.Seek(0, SEEK_SET);
1062 
1063 	if (Logger::IsTraceEnabled()) {
1064 		HDLOGPREFIX(LOG_LEVEL_TRACE)
1065 		printf("%s; response; ", PROTOCOL_NAME);
1066 		_LogPayload(&replyData, replyData.BufferLength());
1067 		printf("\n");
1068 	}
1069 
1070 	BJsonMessageWriter jsonMessageWriter(reply);
1071 	BJson::Parse(&replyData, &jsonMessageWriter);
1072 	status_t status = jsonMessageWriter.ErrorStatus();
1073 
1074 	if (Logger::IsTraceEnabled() && status == B_BAD_DATA) {
1075 		BString resultString(static_cast<const char *>(replyData.Buffer()),
1076 			replyData.BufferLength());
1077 		HDERROR("Parser choked on JSON:\n%s", resultString.String());
1078 	}
1079 	return status;
1080 }
1081 
1082 
1083 status_t
1084 WebAppInterface::_SendJsonRequest(const char* urlPathComponents,
1085 	const BString& jsonString, uint32 flags, BMessage& reply)
1086 {
1087 	// gets 'adopted' by the subsequent http request.
1088 	BMemoryIO* data = new BMemoryIO(jsonString.String(),
1089 		jsonString.Length() - 1);
1090 
1091 	return _SendJsonRequest(urlPathComponents, data, jsonString.Length() - 1,
1092 		flags, reply);
1093 }
1094 
1095 
1096 status_t
1097 WebAppInterface::_SendRawGetRequest(const BString urlPathComponents,
1098 	BDataIO* stream)
1099 {
1100 	BUrl url = ServerSettings::CreateFullUrl(urlPathComponents);
1101 
1102 	HDDEBUG("http-get; will make request to [%s]",
1103 		url.UrlString().String());
1104 
1105 	ProtocolListener listener;
1106 
1107 	BHttpHeaders headers;
1108 	ServerSettings::AugmentHeaders(headers);
1109 
1110 	BHttpRequest *request = make_http_request(url, stream, &listener);
1111 	ObjectDeleter<BHttpRequest> _(request);
1112 	if (request == NULL)
1113 		return B_ERROR;
1114 	request->SetMethod(B_HTTP_GET);
1115 	request->SetHeaders(headers);
1116 
1117 	thread_id thread = request->Run();
1118 	wait_for_thread(thread, NULL);
1119 
1120 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
1121 		request->Result());
1122 
1123 	int32 statusCode = result.StatusCode();
1124 
1125 	HDDEBUG("http-get; did receive http-status [%" B_PRId32 "] from [%s]",
1126 		statusCode, url.UrlString().String());
1127 
1128 	if (statusCode == 200)
1129 		return B_OK;
1130 
1131 	HDERROR("failed to get data from '%s': %" B_PRIi32 "",
1132 		url.UrlString().String(), statusCode);
1133 	return B_ERROR;
1134 }
1135 
1136 
1137 void
1138 WebAppInterface::_LogPayload(BPositionIO* requestData, size_t size)
1139 {
1140 	off_t requestDataOffset = requestData->Position();
1141 	char buffer[LOG_PAYLOAD_LIMIT];
1142 
1143 	if (size > LOG_PAYLOAD_LIMIT)
1144 		size = LOG_PAYLOAD_LIMIT;
1145 
1146 	if (B_OK != requestData->ReadExactly(buffer, size)) {
1147 		printf("%s; error logging payload", PROTOCOL_NAME);
1148 	} else {
1149 		for (uint32 i = 0; i < size; i++) {
1150     		bool esc = buffer[i] > 126 ||
1151     			(buffer[i] < 0x20 && buffer[i] != 0x0a);
1152 
1153     		if (esc)
1154     			printf("\\u%02x", buffer[i]);
1155     		else
1156     			putchar(buffer[i]);
1157     	}
1158 
1159     	if (size == LOG_PAYLOAD_LIMIT)
1160     		printf("...(continues)");
1161 	}
1162 
1163 	requestData->Seek(requestDataOffset, SEEK_SET);
1164 }
1165 
1166 
1167 /*!	This will get the position of the data to get the length an then sets the
1168 	offset to zero so that it can be re-read for reading the payload in to log
1169 	or send.
1170 */
1171 
1172 off_t
1173 WebAppInterface::_LengthAndSeekToZero(BPositionIO* data)
1174 {
1175 	off_t dataSize = data->Position();
1176     data->Seek(0, SEEK_SET);
1177     return dataSize;
1178 }
1179 
1180 
1181 UserCredentials
1182 WebAppInterface::_Credentials()
1183 {
1184 	AutoLocker<BLocker> lock(&fLock);
1185 	return fCredentials;
1186 }
1187 
1188 
1189 AccessToken
1190 WebAppInterface::_ObtainValidAccessToken()
1191 {
1192 	AutoLocker<BLocker> lock(&fLock);
1193 
1194 	uint64 now = static_cast<uint64>(time(NULL)) * 1000;
1195 
1196 	if (!fAccessToken.IsValid(now)) {
1197 		HDINFO("clearing cached access token as it is no longer valid");
1198 		fAccessToken.Clear();
1199 	}
1200 
1201 	if (!fAccessToken.IsValid()) {
1202 		HDINFO("no cached access token present; will obtain a new one");
1203 		AuthenticateUserRetainingAccessToken();
1204 	}
1205 
1206 	return fAccessToken;
1207 }
1208