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
43class JsonBuilder {
44public:
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
165private:
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
190private:
191	BString		fString;
192	bool		fInList;
193};
194
195
196class ProtocolListener : public BUrlProtocolListener {
197public:
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
258private:
259	BDataIO*		fDownloadIO;
260	bool			fTraceLogging;
261};
262
263
264int
265WebAppInterface::fRequestIndex = 0;
266
267
268enum {
269	NEEDS_AUTHORIZATION = 1 << 0,
270};
271
272
273WebAppInterface::WebAppInterface()
274{
275}
276
277
278WebAppInterface::WebAppInterface(const WebAppInterface& other)
279	:
280	fCredentials(other.fCredentials)
281{
282}
283
284
285WebAppInterface::~WebAppInterface()
286{
287}
288
289
290WebAppInterface&
291WebAppInterface::operator=(const WebAppInterface& other)
292{
293	if (this == &other)
294		return *this;
295	fCredentials = other.fCredentials;
296	return *this;
297}
298
299
300void
301WebAppInterface::SetAuthorization(const UserCredentials& value)
302{
303	fCredentials = value;
304}
305
306
307const BString&
308WebAppInterface::Nickname() const
309{
310	return fCredentials.Nickname();
311}
312
313
314status_t
315WebAppInterface::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
332status_t
333WebAppInterface::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
370status_t
371WebAppInterface::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
438status_t
439WebAppInterface::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
479status_t
480WebAppInterface::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
492WebAppInterface::_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
550status_t
551WebAppInterface::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
595status_t
596WebAppInterface::_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
626status_t
627WebAppInterface::_RetrieveUserUsageConditionsCopy(const BString& code,
628	BDataIO* stream)
629{
630	return _SendRawGetRequest(
631		BString("/__user/usageconditions/") << code << "/document.md",
632		stream);
633}
634
635
636status_t
637WebAppInterface::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
717status_t
718WebAppInterface::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
776status_t
777WebAppInterface::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
786status_t
787WebAppInterface::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
803status_t
804WebAppInterface::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
848status_t
849WebAppInterface::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
876int32
877WebAppInterface::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
894void
895WebAppInterface::_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
907status_t
908WebAppInterface::_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
916status_t
917WebAppInterface::_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
1037status_t
1038WebAppInterface::_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
1050status_t
1051WebAppInterface::_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
1084void
1085WebAppInterface::_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
1119off_t
1120WebAppInterface::_LengthAndSeekToZero(BPositionIO* data)
1121{
1122	off_t dataSize = data->Position();
1123    data->Seek(0, SEEK_SET);
1124    return dataSize;
1125}
1126