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