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