xref: /haiku/src/apps/haikudepot/server/WebAppInterface.cpp (revision baa4caf76bc37996df1e98ae7791b25ac4d24402)
1 /*
2  * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2016-2019, 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, UserDetail& userDetail)
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 	BMessage responseEnvelopeMessage;
463 	status_t result = _SendJsonRequest("user", credentials, requestEnvelopeData,
464 		_LengthAndSeekToZero(requestEnvelopeData), NEEDS_AUTHORIZATION,
465 		responseEnvelopeMessage);
466 		// note that the credentials used here are passed in as args.
467 
468 	if (result == B_OK)
469 		result = _UnpackUserDetails(responseEnvelopeMessage, userDetail);
470 
471 	return result;
472 }
473 
474 
475 /*! This method will return the credentials for the currently authenticated
476     user.
477 */
478 
479 status_t
480 WebAppInterface::RetrieveCurrentUserDetail(UserDetail& userDetail)
481 {
482 	return RetrieveUserDetailForCredentials(fCredentials, userDetail);
483 }
484 
485 
486 /*! When the user requests user detail, the server sends back an envelope of
487     response data.  This method will unpack the data into a model object.
488     \return Not B_OK if something went wrong.
489 */
490 
491 /*static*/ status_t
492 WebAppInterface::_UnpackUserDetails(BMessage& responseEnvelopeMessage,
493 	UserDetail& userDetail)
494 {
495 	BMessage resultMessage;
496 	status_t result = responseEnvelopeMessage.FindMessage(
497 		"result", &resultMessage);
498 
499 	if (result != B_OK) {
500 		fprintf(stderr, "bad response envelope missing 'result' entry\n");
501 		return result;
502 	}
503 
504 	BString nickname;
505 	result = resultMessage.FindString("nickname", &nickname);
506 	userDetail.SetNickname(nickname);
507 
508 	BMessage agreementMessage;
509 	if (resultMessage.FindMessage("userUsageConditionsAgreement",
510 		&agreementMessage) == B_OK) {
511 		BString code;
512 		BDateTime agreedToTimestamp;
513 		BString userUsageConditionsCode;
514 		UserUsageConditionsAgreement agreement = userDetail.Agreement();
515 		bool isLatest;
516 
517 		if (agreementMessage.FindString("userUsageConditionsCode",
518 			&userUsageConditionsCode) == B_OK) {
519 			agreement.SetCode(userUsageConditionsCode);
520 		}
521 
522 		double timestampAgreedMillis;
523 		if (agreementMessage.FindDouble("timestampAgreed",
524 			&timestampAgreedMillis) == B_OK) {
525 			agreement.SetTimestampAgreed((uint64) timestampAgreedMillis);
526 		}
527 
528 		if (agreementMessage.FindBool("isLatest", &isLatest)
529 			== B_OK) {
530 			agreement.SetIsLatest(isLatest);
531 		}
532 
533 		userDetail.SetAgreement(agreement);
534 	}
535 
536 	return result;
537 }
538 
539 
540 /*! \brief Returns data relating to the user usage conditions
541 
542 	\param code defines the version of the data to return or if empty then the
543 		latest is returned.
544 
545     This method will go to the server and get details relating to the user usage
546     conditions.  It does this in two API calls; first gets the details (the
547     minimum age) and in the second call, the text of the conditions is returned.
548 */
549 
550 status_t
551 WebAppInterface::RetrieveUserUsageConditions(const BString& code,
552 	UserUsageConditions& conditions)
553 {
554 	BMessage responseEnvelopeMessage;
555 	status_t result = _RetrieveUserUsageConditionsMeta(code,
556 		responseEnvelopeMessage);
557 
558 	if (result != B_OK)
559 		return result;
560 
561 	BMessage resultMessage;
562 	if (responseEnvelopeMessage.FindMessage("result", &resultMessage) != B_OK) {
563 		fprintf(stderr, "bad response envelope missing 'result' entry\n");
564 		return B_BAD_DATA;
565 	}
566 
567 	BString metaDataCode;
568 	double metaDataMinimumAge;
569 	BString copyMarkdown;
570 
571 	if ( (resultMessage.FindString("code", &metaDataCode) != B_OK)
572 		|| (resultMessage.FindDouble(
573 			"minimumAge", &metaDataMinimumAge) != B_OK) ) {
574 		printf("unexpected response from server with missing user usage "
575 			"conditions data\n");
576 		return B_BAD_DATA;
577 	}
578 
579 	BMallocIO* copyMarkdownData = new BMallocIO();
580 	result = _RetrieveUserUsageConditionsCopy(metaDataCode, copyMarkdownData);
581 
582 	if (result != B_OK)
583 		return result;
584 
585 	conditions.SetCode(metaDataCode);
586 	conditions.SetMinimumAge(metaDataMinimumAge);
587 	conditions.SetCopyMarkdown(
588 		BString(static_cast<const char*>(copyMarkdownData->Buffer()),
589 			copyMarkdownData->BufferLength()));
590 
591 	return B_OK;
592 }
593 
594 
595 status_t
596 WebAppInterface::_RetrieveUserUsageConditionsMeta(const BString& code,
597 	BMessage& message)
598 {
599 	BMallocIO* requestEnvelopeData = new BMallocIO();
600 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
601 
602 	requestEnvelopeWriter.WriteObjectStart();
603 	_WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter,
604 		"getUserUsageConditions");
605 	requestEnvelopeWriter.WriteObjectName("params");
606 	requestEnvelopeWriter.WriteArrayStart();
607 
608 	requestEnvelopeWriter.WriteObjectStart();
609 
610 	if (!code.IsEmpty()) {
611 		requestEnvelopeWriter.WriteObjectName("code");
612 		requestEnvelopeWriter.WriteString(code.String());
613 	}
614 
615 	requestEnvelopeWriter.WriteObjectEnd();
616 	requestEnvelopeWriter.WriteArrayEnd();
617 	requestEnvelopeWriter.WriteObjectEnd();
618 
619 	// now fetch this information into an object.
620 
621 	return _SendJsonRequest("user", requestEnvelopeData,
622 		_LengthAndSeekToZero(requestEnvelopeData), 0, message);
623 }
624 
625 
626 status_t
627 WebAppInterface::_RetrieveUserUsageConditionsCopy(const BString& code,
628 	BDataIO* stream)
629 {
630 	return _SendRawGetRequest(
631 		BString("/__user/usageconditions/") << code << "/document.md",
632 		stream);
633 }
634 
635 
636 status_t
637 WebAppInterface::CreateUserRating(const BString& packageName,
638 	const BPackageVersion& version,
639 	const BString& architecture, const BString& repositoryCode,
640 	const BString& languageCode, const BString& comment,
641 	const BString& stability, int rating, BMessage& message)
642 {
643 		// BHttpRequest later takes ownership of this.
644 	BMallocIO* requestEnvelopeData = new BMallocIO();
645 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
646 
647 	requestEnvelopeWriter.WriteObjectStart();
648 	_WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter,
649 		"createUserRating");
650 	requestEnvelopeWriter.WriteObjectName("params");
651 	requestEnvelopeWriter.WriteArrayStart();
652 
653 	requestEnvelopeWriter.WriteObjectStart();
654 	requestEnvelopeWriter.WriteObjectName("pkgName");
655 	requestEnvelopeWriter.WriteString(packageName.String());
656 	requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode");
657 	requestEnvelopeWriter.WriteString(architecture.String());
658 	requestEnvelopeWriter.WriteObjectName("repositoryCode");
659 	requestEnvelopeWriter.WriteString(repositoryCode.String());
660 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
661 	requestEnvelopeWriter.WriteString(languageCode.String());
662 	requestEnvelopeWriter.WriteObjectName("pkgVersionType");
663 	requestEnvelopeWriter.WriteString("SPECIFIC");
664 	requestEnvelopeWriter.WriteObjectName("userNickname");
665 	requestEnvelopeWriter.WriteString(fCredentials.Nickname());
666 
667 	if (!version.Major().IsEmpty()) {
668 		requestEnvelopeWriter.WriteObjectName("pkgVersionMajor");
669 		requestEnvelopeWriter.WriteString(version.Major());
670 	}
671 
672 	if (!version.Minor().IsEmpty()) {
673 		requestEnvelopeWriter.WriteObjectName("pkgVersionMinor");
674 		requestEnvelopeWriter.WriteString(version.Minor());
675 	}
676 
677 	if (!version.Micro().IsEmpty()) {
678 		requestEnvelopeWriter.WriteObjectName("pkgVersionMicro");
679 		requestEnvelopeWriter.WriteString(version.Micro());
680 	}
681 
682 	if (!version.PreRelease().IsEmpty()) {
683 		requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease");
684 		requestEnvelopeWriter.WriteString(version.PreRelease());
685 	}
686 
687 	if (version.Revision() != 0) {
688 		requestEnvelopeWriter.WriteObjectName("pkgVersionRevision");
689 		requestEnvelopeWriter.WriteInteger(version.Revision());
690 	}
691 
692 	if (rating > 0.0f) {
693 		requestEnvelopeWriter.WriteObjectName("rating");
694     	requestEnvelopeWriter.WriteInteger(rating);
695 	}
696 
697 	if (stability.Length() > 0) {
698 		requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode");
699 		requestEnvelopeWriter.WriteString(stability);
700 	}
701 
702 	if (comment.Length() > 0) {
703 		requestEnvelopeWriter.WriteObjectName("comment");
704 		requestEnvelopeWriter.WriteString(comment.String());
705 	}
706 
707 	requestEnvelopeWriter.WriteObjectEnd();
708 	requestEnvelopeWriter.WriteArrayEnd();
709 	requestEnvelopeWriter.WriteObjectEnd();
710 
711 	return _SendJsonRequest("userrating", requestEnvelopeData,
712 		_LengthAndSeekToZero(requestEnvelopeData), NEEDS_AUTHORIZATION,
713 		message);
714 }
715 
716 
717 status_t
718 WebAppInterface::UpdateUserRating(const BString& ratingID,
719 	const BString& languageCode, const BString& comment,
720 	const BString& stability, int rating, bool active, BMessage& message)
721 {
722 		// BHttpRequest later takes ownership of this.
723 	BMallocIO* requestEnvelopeData = new BMallocIO();
724 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
725 
726 	requestEnvelopeWriter.WriteObjectStart();
727 	_WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter,
728 		"updateUserRating");
729 
730 	requestEnvelopeWriter.WriteObjectName("params");
731 	requestEnvelopeWriter.WriteArrayStart();
732 
733 	requestEnvelopeWriter.WriteObjectStart();
734 
735 	requestEnvelopeWriter.WriteObjectName("code");
736 	requestEnvelopeWriter.WriteString(ratingID.String());
737 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
738 	requestEnvelopeWriter.WriteString(languageCode.String());
739 	requestEnvelopeWriter.WriteObjectName("active");
740 	requestEnvelopeWriter.WriteBoolean(active);
741 
742 	requestEnvelopeWriter.WriteObjectName("filter");
743 	requestEnvelopeWriter.WriteArrayStart();
744 	requestEnvelopeWriter.WriteString("ACTIVE");
745 	requestEnvelopeWriter.WriteString("NATURALLANGUAGE");
746 	requestEnvelopeWriter.WriteString("USERRATINGSTABILITY");
747 	requestEnvelopeWriter.WriteString("COMMENT");
748 	requestEnvelopeWriter.WriteString("RATING");
749 	requestEnvelopeWriter.WriteArrayEnd();
750 
751 	if (rating >= 0) {
752 		requestEnvelopeWriter.WriteObjectName("rating");
753 		requestEnvelopeWriter.WriteInteger(rating);
754 	}
755 
756 	if (stability.Length() > 0) {
757 		requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode");
758 		requestEnvelopeWriter.WriteString(stability);
759 	}
760 
761 	if (comment.Length() > 0) {
762 		requestEnvelopeWriter.WriteObjectName("comment");
763 		requestEnvelopeWriter.WriteString(comment);
764 	}
765 
766 	requestEnvelopeWriter.WriteObjectEnd();
767 	requestEnvelopeWriter.WriteArrayEnd();
768 	requestEnvelopeWriter.WriteObjectEnd();
769 
770 	return _SendJsonRequest("userrating", requestEnvelopeData,
771 		_LengthAndSeekToZero(requestEnvelopeData), NEEDS_AUTHORIZATION,
772 		message);
773 }
774 
775 
776 status_t
777 WebAppInterface::RetrieveScreenshot(const BString& code,
778 	int32 width, int32 height, BDataIO* stream)
779 {
780 	return _SendRawGetRequest(
781 		BString("/__pkgscreenshot/") << code << ".png" << "?tw="
782 			<< width << "&th=" << height, stream);
783 }
784 
785 
786 status_t
787 WebAppInterface::RequestCaptcha(BMessage& message)
788 {
789 	BString jsonString = JsonBuilder()
790 		.AddValue("jsonrpc", "2.0")
791 		.AddValue("id", ++fRequestIndex)
792 		.AddValue("method", "generateCaptcha")
793 		.AddArray("params")
794 			.AddObject()
795 			.EndObject()
796 		.EndArray()
797 	.End();
798 
799 	return _SendJsonRequest("captcha", jsonString, 0, message);
800 }
801 
802 
803 status_t
804 WebAppInterface::CreateUser(const BString& nickName,
805 	const BString& passwordClear, const BString& email,
806 	const BString& captchaToken, const BString& captchaResponse,
807 	const BString& languageCode, const BString& userUsageConditionsCode,
808 	BMessage& message)
809 {
810 		// BHttpRequest later takes ownership of this.
811 	BMallocIO* requestEnvelopeData = new BMallocIO();
812 	BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData);
813 
814 	requestEnvelopeWriter.WriteObjectStart();
815 	_WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, "createUser");
816 	requestEnvelopeWriter.WriteObjectName("params");
817 	requestEnvelopeWriter.WriteArrayStart();
818 
819 	requestEnvelopeWriter.WriteObjectStart();
820 
821 	requestEnvelopeWriter.WriteObjectName("nickname");
822 	requestEnvelopeWriter.WriteString(nickName.String());
823 	requestEnvelopeWriter.WriteObjectName("passwordClear");
824 	requestEnvelopeWriter.WriteString(passwordClear.String());
825 	requestEnvelopeWriter.WriteObjectName("captchaToken");
826 	requestEnvelopeWriter.WriteString(captchaToken.String());
827 	requestEnvelopeWriter.WriteObjectName("captchaResponse");
828 	requestEnvelopeWriter.WriteString(captchaResponse.String());
829 	requestEnvelopeWriter.WriteObjectName("naturalLanguageCode");
830 	requestEnvelopeWriter.WriteString(languageCode.String());
831 	requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode");
832 	requestEnvelopeWriter.WriteString(userUsageConditionsCode.String());
833 
834 	if (!email.IsEmpty()) {
835 		requestEnvelopeWriter.WriteObjectName("email");
836 		requestEnvelopeWriter.WriteString(email.String());
837 	}
838 
839 	requestEnvelopeWriter.WriteObjectEnd();
840 	requestEnvelopeWriter.WriteArrayEnd();
841 	requestEnvelopeWriter.WriteObjectEnd();
842 
843 	return _SendJsonRequest("user", requestEnvelopeData,
844 		_LengthAndSeekToZero(requestEnvelopeData), 0, message);
845 }
846 
847 
848 status_t
849 WebAppInterface::AuthenticateUser(const BString& nickName,
850 	const BString& passwordClear, BMessage& message)
851 {
852 	BString jsonString = JsonBuilder()
853 		.AddValue("jsonrpc", "2.0")
854 		.AddValue("id", ++fRequestIndex)
855 		.AddValue("method", "authenticateUser")
856 		.AddArray("params")
857 			.AddObject()
858 				.AddValue("nickname", nickName)
859 				.AddValue("passwordClear", passwordClear)
860 			.EndObject()
861 		.EndArray()
862 	.End();
863 
864 	return _SendJsonRequest("user", jsonString, 0, message);
865 }
866 
867 
868 /*! JSON-RPC invocations return a response.  The response may be either
869     a result or it may be an error depending on the response structure.
870     If it is an error then there may be additional detail that is the
871     error code and message.  This method will extract the error code
872     from the response.  This method will return 0 if the payload does
873     not look like an error.
874 */
875 
876 int32
877 WebAppInterface::ErrorCodeFromResponse(BMessage& response)
878 {
879 	BMessage error;
880 	double code;
881 
882 	if (response.FindMessage("error", &error) == B_OK
883 		&& error.FindDouble("code", &code) == B_OK) {
884 		return (int32) code;
885 	}
886 
887 	return 0;
888 }
889 
890 
891 // #pragma mark - private
892 
893 
894 void
895 WebAppInterface::_WriteStandardJsonRpcEnvelopeValues(BJsonWriter& writer,
896 	const char* methodName)
897 {
898 	writer.WriteObjectName("jsonrpc");
899 	writer.WriteString("2.0");
900 	writer.WriteObjectName("id");
901 	writer.WriteInteger(++fRequestIndex);
902 	writer.WriteObjectName("method");
903 	writer.WriteString(methodName);
904 }
905 
906 
907 status_t
908 WebAppInterface::_SendJsonRequest(const char* domain, BPositionIO* requestData,
909 	size_t requestDataSize, uint32 flags, BMessage& reply) const
910 {
911 	return _SendJsonRequest(domain, fCredentials, requestData, requestDataSize,
912 		flags, reply);
913 }
914 
915 
916 status_t
917 WebAppInterface::_SendJsonRequest(const char* domain,
918 	UserCredentials credentials, BPositionIO* requestData,
919 	size_t requestDataSize, uint32 flags, BMessage& reply) const
920 {
921 	if (requestDataSize == 0) {
922 		if (Logger::IsInfoEnabled())
923 			printf("jrpc; empty request payload\n");
924 		return B_ERROR;
925 	}
926 
927 	if (!ServerHelper::IsNetworkAvailable()) {
928 		if (Logger::IsDebugEnabled()) {
929 			printf("jrpc; dropping request to ...[%s] as network is not "
930 				"available\n", domain);
931 		}
932 		delete requestData;
933 		return HD_NETWORK_INACCESSIBLE;
934 	}
935 
936 	if (ServerSettings::IsClientTooOld()) {
937 		if (Logger::IsDebugEnabled()) {
938 			printf("jrpc; dropping request to ...[%s] as client is too "
939 				"old\n", domain);
940 		}
941 		delete requestData;
942 		return HD_CLIENT_TOO_OLD;
943 	}
944 
945 	BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v1/") << domain);
946 	bool isSecure = url.Protocol() == "https";
947 
948 	if (Logger::IsDebugEnabled()) {
949 		printf("jrpc; will make request to [%s]\n",
950 			url.UrlString().String());
951 	}
952 
953 	// If the request payload is logged then it must be copied to local memory
954 	// from the stream.  This then requires that the request data is then
955 	// delivered from memory.
956 
957 	if (Logger::IsTraceEnabled()) {
958 		printf("jrpc request; ");
959 		_LogPayload(requestData, requestDataSize);
960 		printf("\n");
961 	}
962 
963 	ProtocolListener listener(Logger::IsTraceEnabled());
964 	BUrlContext context;
965 
966 	BHttpHeaders headers;
967 	headers.AddHeader("Content-Type", "application/json");
968 	ServerSettings::AugmentHeaders(headers);
969 
970 	BHttpRequest request(url, isSecure, "HTTP", &listener, &context);
971 	request.SetMethod(B_HTTP_POST);
972 	request.SetHeaders(headers);
973 
974 	// Authentication via Basic Authentication
975 	// The other way would be to obtain a token and then use the Token Bearer
976 	// header.
977 	if (((flags & NEEDS_AUTHORIZATION) != 0) && credentials.IsValid()) {
978 		BHttpAuthentication authentication(credentials.Nickname(),
979 			credentials.PasswordClear());
980 		authentication.SetMethod(B_HTTP_AUTHENTICATION_BASIC);
981 		context.AddAuthentication(url, authentication);
982 	}
983 
984 	request.AdoptInputData(requestData, requestDataSize);
985 
986 	BMallocIO replyData;
987 	listener.SetDownloadIO(&replyData);
988 
989 	thread_id thread = request.Run();
990 	wait_for_thread(thread, NULL);
991 
992 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
993 		request.Result());
994 
995 	int32 statusCode = result.StatusCode();
996 
997 	if (Logger::IsDebugEnabled()) {
998 		printf("jrpc; did receive http-status [%" B_PRId32 "] "
999 			"from [%s]\n", statusCode, url.UrlString().String());
1000 	}
1001 
1002 	switch (statusCode) {
1003 		case B_HTTP_STATUS_OK:
1004 			break;
1005 
1006 		case B_HTTP_STATUS_PRECONDITION_FAILED:
1007 			ServerHelper::NotifyClientTooOld(result.Headers());
1008 			return HD_CLIENT_TOO_OLD;
1009 
1010 		default:
1011 			printf("jrpc request to endpoint [.../%s] failed with http "
1012 				"status [%" B_PRId32 "]\n", domain, statusCode);
1013 			return B_ERROR;
1014 	}
1015 
1016 	replyData.Seek(0, SEEK_SET);
1017 
1018 	if (Logger::IsTraceEnabled()) {
1019 		printf("jrpc response; ");
1020 		_LogPayload(&replyData, replyData.BufferLength());
1021 		printf("\n");
1022 	}
1023 
1024 	BJsonMessageWriter jsonMessageWriter(reply);
1025 	BJson::Parse(&replyData, &jsonMessageWriter);
1026 	status_t status = jsonMessageWriter.ErrorStatus();
1027 
1028 	if (Logger::IsTraceEnabled() && status == B_BAD_DATA) {
1029 		BString resultString(static_cast<const char *>(replyData.Buffer()),
1030 			replyData.BufferLength());
1031 		printf("Parser choked on JSON:\n%s\n", resultString.String());
1032 	}
1033 	return status;
1034 }
1035 
1036 
1037 status_t
1038 WebAppInterface::_SendJsonRequest(const char* domain, const BString& jsonString,
1039 	uint32 flags, BMessage& reply) const
1040 {
1041 	// gets 'adopted' by the subsequent http request.
1042 	BMemoryIO* data = new BMemoryIO(jsonString.String(),
1043 		jsonString.Length() - 1);
1044 
1045 	return _SendJsonRequest(domain, data, jsonString.Length() - 1, flags,
1046 		reply);
1047 }
1048 
1049 
1050 status_t
1051 WebAppInterface::_SendRawGetRequest(const BString urlPathComponents,
1052 	BDataIO* stream)
1053 {
1054 	BUrl url = ServerSettings::CreateFullUrl(urlPathComponents);
1055 	bool isSecure = url.Protocol() == "https";
1056 
1057 	ProtocolListener listener(Logger::IsTraceEnabled());
1058 	listener.SetDownloadIO(stream);
1059 
1060 	BHttpHeaders headers;
1061 	ServerSettings::AugmentHeaders(headers);
1062 
1063 	BHttpRequest request(url, isSecure, "HTTP", &listener);
1064 	request.SetMethod(B_HTTP_GET);
1065 	request.SetHeaders(headers);
1066 
1067 	thread_id thread = request.Run();
1068 	wait_for_thread(thread, NULL);
1069 
1070 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
1071 		request.Result());
1072 
1073 	int32 statusCode = result.StatusCode();
1074 
1075 	if (statusCode == 200)
1076 		return B_OK;
1077 
1078 	fprintf(stderr, "failed to get data from '%s': %" B_PRIi32 "\n",
1079 		url.UrlString().String(), statusCode);
1080 	return B_ERROR;
1081 }
1082 
1083 
1084 void
1085 WebAppInterface::_LogPayload(BPositionIO* requestData, size_t size)
1086 {
1087 	off_t requestDataOffset = requestData->Position();
1088 	char buffer[LOG_PAYLOAD_LIMIT];
1089 
1090 	if (size > LOG_PAYLOAD_LIMIT)
1091 		size = LOG_PAYLOAD_LIMIT;
1092 
1093 	if (B_OK != requestData->ReadExactly(buffer, size)) {
1094 		printf("jrpc; error logging payload\n");
1095 	} else {
1096 		for (uint32 i = 0; i < size; i++) {
1097     		bool esc = buffer[i] > 126 ||
1098     			(buffer[i] < 0x20 && buffer[i] != 0x0a);
1099 
1100     		if (esc)
1101     			printf("\\u%02x", buffer[i]);
1102     		else
1103     			putchar(buffer[i]);
1104     	}
1105 
1106     	if (size == LOG_PAYLOAD_LIMIT)
1107     		printf("...(continues)");
1108 	}
1109 
1110 	requestData->Seek(requestDataOffset, SEEK_SET);
1111 }
1112 
1113 
1114 /*! This will get the position of the data to get the length an then sets the
1115     offset to zero so that it can be re-read for reading the payload in to log
1116     or send.
1117 */
1118 
1119 off_t
1120 WebAppInterface::_LengthAndSeekToZero(BPositionIO* data)
1121 {
1122 	off_t dataSize = data->Position();
1123     data->Seek(0, SEEK_SET);
1124     return dataSize;
1125 }
1126