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