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