xref: /haiku/src/apps/haikudepot/server/WebAppInterface.cpp (revision 6aff37d1c79e20748c683ae224bd629f88a5b0be)
1 /*
2  * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2016-2017, 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 <Autolock.h>
14 #include <File.h>
15 #include <HttpHeaders.h>
16 #include <HttpRequest.h>
17 #include <Json.h>
18 #include <Message.h>
19 #include <Roster.h>
20 #include <Url.h>
21 #include <UrlContext.h>
22 #include <UrlProtocolListener.h>
23 #include <UrlProtocolRoster.h>
24 
25 #include "AutoLocker.h"
26 #include "List.h"
27 #include "PackageInfo.h"
28 #include "ServerSettings.h"
29 
30 
31 #define BASEURL_DEFAULT "https://depot.haiku-os.org"
32 #define USERAGENT_FALLBACK_VERSION "0.0.0"
33 
34 
35 class JsonBuilder {
36 public:
37 	JsonBuilder()
38 		:
39 		fString("{"),
40 		fInList(false)
41 	{
42 	}
43 
44 	JsonBuilder& AddObject()
45 	{
46 		fString << '{';
47 		fInList = false;
48 		return *this;
49 	}
50 
51 	JsonBuilder& AddObject(const char* name)
52 	{
53 		_StartName(name);
54 		fString << '{';
55 		fInList = false;
56 		return *this;
57 	}
58 
59 	JsonBuilder& EndObject()
60 	{
61 		fString << '}';
62 		fInList = true;
63 		return *this;
64 	}
65 
66 	JsonBuilder& AddArray(const char* name)
67 	{
68 		_StartName(name);
69 		fString << '[';
70 		fInList = false;
71 		return *this;
72 	}
73 
74 	JsonBuilder& EndArray()
75 	{
76 		fString << ']';
77 		fInList = true;
78 		return *this;
79 	}
80 
81 	JsonBuilder& AddStrings(const StringList& strings)
82 	{
83 		for (int i = 0; i < strings.CountItems(); i++)
84 			AddItem(strings.ItemAtFast(i));
85 		return *this;
86 	}
87 
88 	JsonBuilder& AddItem(const char* item)
89 	{
90 		return AddItem(item, false);
91 	}
92 
93 	JsonBuilder& AddItem(const char* item, bool nullIfEmpty)
94 	{
95 		if (item == NULL || (nullIfEmpty && strlen(item) == 0)) {
96 			if (fInList)
97 				fString << ",null";
98 			else
99 				fString << "null";
100 		} else {
101 			if (fInList)
102 				fString << ",\"";
103 			else
104 				fString << '"';
105 			fString << _EscapeString(item);
106 			fString << '"';
107 		}
108 		fInList = true;
109 		return *this;
110 	}
111 
112 	JsonBuilder& AddValue(const char* name, const char* value)
113 	{
114 		return AddValue(name, value, false);
115 	}
116 
117 	JsonBuilder& AddValue(const char* name, const char* value,
118 		bool nullIfEmpty)
119 	{
120 		_StartName(name);
121 		if (value == NULL || (nullIfEmpty && strlen(value) == 0)) {
122 			fString << "null";
123 		} else {
124 			fString << '"';
125 			fString << _EscapeString(value);
126 			fString << '"';
127 		}
128 		fInList = true;
129 		return *this;
130 	}
131 
132 	JsonBuilder& AddValue(const char* name, int value)
133 	{
134 		_StartName(name);
135 		fString << value;
136 		fInList = true;
137 		return *this;
138 	}
139 
140 	JsonBuilder& AddValue(const char* name, bool value)
141 	{
142 		_StartName(name);
143 		if (value)
144 			fString << "true";
145 		else
146 			fString << "false";
147 		fInList = true;
148 		return *this;
149 	}
150 
151 	const BString& End()
152 	{
153 		fString << "}\n";
154 		return fString;
155 	}
156 
157 private:
158 	void _StartName(const char* name)
159 	{
160 		if (fInList)
161 			fString << ",\"";
162 		else
163 			fString << '"';
164 		fString << _EscapeString(name);
165 		fString << "\":";
166 	}
167 
168 	BString _EscapeString(const char* original) const
169 	{
170 		BString string(original);
171 		string.ReplaceAll("\\", "\\\\");
172 		string.ReplaceAll("\"", "\\\"");
173 		string.ReplaceAll("/", "\\/");
174 		string.ReplaceAll("\b", "\\b");
175 		string.ReplaceAll("\f", "\\f");
176 		string.ReplaceAll("\n", "\\n");
177 		string.ReplaceAll("\r", "\\r");
178 		string.ReplaceAll("\t", "\\t");
179 		return string;
180 	}
181 
182 private:
183 	BString		fString;
184 	bool		fInList;
185 };
186 
187 
188 class ProtocolListener : public BUrlProtocolListener {
189 public:
190 	ProtocolListener(bool traceLogging)
191 		:
192 		fDownloadIO(NULL),
193 		fTraceLogging(traceLogging)
194 	{
195 	}
196 
197 	virtual ~ProtocolListener()
198 	{
199 	}
200 
201 	virtual	void ConnectionOpened(BUrlRequest* caller)
202 	{
203 	}
204 
205 	virtual void HostnameResolved(BUrlRequest* caller, const char* ip)
206 	{
207 	}
208 
209 	virtual void ResponseStarted(BUrlRequest* caller)
210 	{
211 	}
212 
213 	virtual void HeadersReceived(BUrlRequest* caller, const BUrlResult& result)
214 	{
215 	}
216 
217 	virtual void DataReceived(BUrlRequest* caller, const char* data,
218 		off_t position, ssize_t size)
219 	{
220 		if (fDownloadIO != NULL)
221 			fDownloadIO->Write(data, size);
222 	}
223 
224 	virtual	void DownloadProgress(BUrlRequest* caller, ssize_t bytesReceived,
225 		ssize_t bytesTotal)
226 	{
227 	}
228 
229 	virtual void UploadProgress(BUrlRequest* caller, ssize_t bytesSent,
230 		ssize_t bytesTotal)
231 	{
232 	}
233 
234 	virtual void RequestCompleted(BUrlRequest* caller, bool success)
235 	{
236 	}
237 
238 	virtual void DebugMessage(BUrlRequest* caller,
239 		BUrlProtocolDebugMessage type, const char* text)
240 	{
241 		if (fTraceLogging)
242 			printf("jrpc: %s\n", text);
243 	}
244 
245 	void SetDownloadIO(BDataIO* downloadIO)
246 	{
247 		fDownloadIO = downloadIO;
248 	}
249 
250 private:
251 	BDataIO*		fDownloadIO;
252 	bool			fTraceLogging;
253 };
254 
255 
256 int
257 WebAppInterface::fRequestIndex = 0;
258 
259 
260 enum {
261 	NEEDS_AUTHORIZATION = 1 << 0,
262 };
263 
264 
265 WebAppInterface::WebAppInterface()
266 	:
267 	fLanguage("en")
268 {
269 }
270 
271 
272 WebAppInterface::WebAppInterface(const WebAppInterface& other)
273 	:
274 	fUsername(other.fUsername),
275 	fPassword(other.fPassword),
276 	fLanguage(other.fLanguage)
277 {
278 }
279 
280 
281 WebAppInterface::~WebAppInterface()
282 {
283 }
284 
285 
286 WebAppInterface&
287 WebAppInterface::operator=(const WebAppInterface& other)
288 {
289 	if (this == &other)
290 		return *this;
291 
292 	fUsername = other.fUsername;
293 	fPassword = other.fPassword;
294 	fLanguage = other.fLanguage;
295 
296 	return *this;
297 }
298 
299 
300 void
301 WebAppInterface::SetAuthorization(const BString& username,
302 	const BString& password)
303 {
304 	fUsername = username;
305 	fPassword = password;
306 }
307 
308 
309 void
310 WebAppInterface::SetPreferredLanguage(const BString& language)
311 {
312 	fLanguage = language;
313 }
314 
315 
316 status_t
317 WebAppInterface::RetrieveRepositoriesForSourceBaseURLs(
318 	const StringList& repositorySourceBaseURLs,
319 	BMessage& message)
320 {
321 	BString jsonString = JsonBuilder()
322 		.AddValue("jsonrpc", "2.0")
323 		.AddValue("id", ++fRequestIndex)
324 		.AddValue("method", "searchRepositories")
325 		.AddArray("params")
326 			.AddObject()
327 				.AddArray("repositorySourceSearchUrls")
328 					.AddStrings(repositorySourceBaseURLs)
329 				.EndArray()
330 				.AddValue("offset", 0)
331 				.AddValue("limit", 1000) // effectively a safety limit
332 			.EndObject()
333 		.EndArray()
334 	.End();
335 
336 	return _SendJsonRequest("repository", jsonString, 0, message);
337 }
338 
339 
340 status_t
341 WebAppInterface::RetrievePackageInfo(const BString& packageName,
342 	const BString& architecture, const BString& repositoryCode,
343 	BMessage& message)
344 {
345 	BString jsonString = JsonBuilder()
346 		.AddValue("jsonrpc", "2.0")
347 		.AddValue("id", ++fRequestIndex)
348 		.AddValue("method", "getPkg")
349 		.AddArray("params")
350 			.AddObject()
351 				.AddValue("name", packageName)
352 				.AddValue("architectureCode", architecture)
353 				.AddValue("naturalLanguageCode", fLanguage)
354 				.AddValue("repositoryCode", repositoryCode)
355 				.AddValue("versionType", "NONE")
356 			.EndObject()
357 		.EndArray()
358 	.End();
359 
360 	return _SendJsonRequest("pkg", jsonString, 0, message);
361 }
362 
363 
364 status_t
365 WebAppInterface::RetrieveBulkPackageInfo(const StringList& packageNames,
366 	const StringList& packageArchitectures,
367 	const StringList& repositoryCodes, BMessage& message)
368 {
369 	BString jsonString = JsonBuilder()
370 		.AddValue("jsonrpc", "2.0")
371 		.AddValue("id", ++fRequestIndex)
372 		.AddValue("method", "getBulkPkg")
373 		.AddArray("params")
374 			.AddObject()
375 				.AddArray("pkgNames")
376 					.AddStrings(packageNames)
377 				.EndArray()
378 				.AddArray("architectureCodes")
379 					.AddStrings(packageArchitectures)
380 				.EndArray()
381 				.AddArray("repositoryCodes")
382 					.AddStrings(repositoryCodes)
383 				.EndArray()
384 				.AddValue("naturalLanguageCode", fLanguage)
385 				.AddValue("versionType", "LATEST")
386 				.AddArray("filter")
387 					.AddItem("PKGCATEGORIES")
388 					.AddItem("PKGSCREENSHOTS")
389 					.AddItem("PKGVERSIONLOCALIZATIONDESCRIPTIONS")
390 					.AddItem("PKGCHANGELOG")
391 				.EndArray()
392 			.EndObject()
393 		.EndArray()
394 	.End();
395 
396 	return _SendJsonRequest("pkg", jsonString, 0, message);
397 }
398 
399 
400 status_t
401 WebAppInterface::RetrieveUserRatings(const BString& packageName,
402 	const BString& architecture, int resultOffset, int maxResults,
403 	BMessage& message)
404 {
405 	BString jsonString = JsonBuilder()
406 		.AddValue("jsonrpc", "2.0")
407 		.AddValue("id", ++fRequestIndex)
408 		.AddValue("method", "searchUserRatings")
409 		.AddArray("params")
410 			.AddObject()
411 				.AddValue("pkgName", packageName)
412 				.AddValue("pkgVersionArchitectureCode", architecture)
413 				.AddValue("offset", resultOffset)
414 				.AddValue("limit", maxResults)
415 			.EndObject()
416 		.EndArray()
417 	.End();
418 
419 	return _SendJsonRequest("userrating", jsonString, 0, message);
420 }
421 
422 
423 status_t
424 WebAppInterface::RetrieveUserRating(const BString& packageName,
425 	const BPackageVersion& version, const BString& architecture,
426 	const BString &repositoryCode, const BString& username,
427 	BMessage& message)
428 {
429 	BString jsonString = JsonBuilder()
430 		.AddValue("jsonrpc", "2.0")
431 		.AddValue("id", ++fRequestIndex)
432 		.AddValue("method", "getUserRatingByUserAndPkgVersion")
433 		.AddArray("params")
434 			.AddObject()
435 				.AddValue("userNickname", username)
436 				.AddValue("pkgName", packageName)
437 				.AddValue("pkgVersionArchitectureCode", architecture)
438 				.AddValue("pkgVersionMajor", version.Major(), true)
439 				.AddValue("pkgVersionMinor", version.Minor(), true)
440 				.AddValue("pkgVersionMicro", version.Micro(), true)
441 				.AddValue("pkgVersionPreRelease", version.PreRelease(), true)
442 				.AddValue("pkgVersionRevision", (int)version.Revision())
443 				.AddValue("repositoryCode", repositoryCode)
444 			.EndObject()
445 		.EndArray()
446 	.End();
447 
448 	return _SendJsonRequest("userrating", jsonString, 0, message);
449 }
450 
451 
452 status_t
453 WebAppInterface::CreateUserRating(const BString& packageName,
454 	const BString& architecture, const BString& repositoryCode,
455 	const BString& languageCode, const BString& comment,
456 	const BString& stability, int rating, BMessage& message)
457 {
458 	BString jsonString = JsonBuilder()
459 		.AddValue("jsonrpc", "2.0")
460 		.AddValue("id", ++fRequestIndex)
461 		.AddValue("method", "createUserRating")
462 		.AddArray("params")
463 			.AddObject()
464 				.AddValue("pkgName", packageName)
465 				.AddValue("pkgVersionArchitectureCode", architecture)
466 				.AddValue("pkgVersionType", "LATEST")
467 				.AddValue("userNickname", fUsername)
468 				.AddValue("rating", rating)
469 				.AddValue("userRatingStabilityCode", stability, true)
470 				.AddValue("comment", comment)
471 				.AddValue("repositoryCode", repositoryCode)
472 				.AddValue("naturalLanguageCode", languageCode)
473 			.EndObject()
474 		.EndArray()
475 	.End();
476 
477 	return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION,
478 		message);
479 }
480 
481 
482 status_t
483 WebAppInterface::UpdateUserRating(const BString& ratingID,
484 	const BString& languageCode, const BString& comment,
485 	const BString& stability, int rating, bool active, BMessage& message)
486 {
487 	BString jsonString = JsonBuilder()
488 		.AddValue("jsonrpc", "2.0")
489 		.AddValue("id", ++fRequestIndex)
490 		.AddValue("method", "updateUserRating")
491 		.AddArray("params")
492 			.AddObject()
493 				.AddValue("code", ratingID)
494 				.AddValue("rating", rating)
495 				.AddValue("userRatingStabilityCode", stability, true)
496 				.AddValue("comment", comment)
497 				.AddValue("naturalLanguageCode", languageCode)
498 				.AddValue("active", active)
499 				.AddArray("filter")
500 					.AddItem("ACTIVE")
501 					.AddItem("NATURALLANGUAGE")
502 					.AddItem("USERRATINGSTABILITY")
503 					.AddItem("COMMENT")
504 					.AddItem("RATING")
505 				.EndArray()
506 			.EndObject()
507 		.EndArray()
508 	.End();
509 
510 	return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION,
511 		message);
512 }
513 
514 
515 status_t
516 WebAppInterface::RetrieveScreenshot(const BString& code,
517 	int32 width, int32 height, BDataIO* stream)
518 {
519 	BUrl url = ServerSettings::CreateFullUrl(
520 		BString("/__pkgscreenshot/") << code << ".png" << "?tw="
521 			<< width << "&th=" << height);
522 
523 	bool isSecure = url.Protocol() == "https";
524 
525 	ProtocolListener listener(
526 		ServerSettings::UrlConnectionTraceLoggingEnabled());
527 	listener.SetDownloadIO(stream);
528 
529 	BHttpHeaders headers;
530 	ServerSettings::AugmentHeaders(headers);
531 
532 	BHttpRequest request(url, isSecure, "HTTP", &listener);
533 	request.SetMethod(B_HTTP_GET);
534 	request.SetHeaders(headers);
535 
536 	thread_id thread = request.Run();
537 	wait_for_thread(thread, NULL);
538 
539 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
540 		request.Result());
541 
542 	int32 statusCode = result.StatusCode();
543 
544 	if (statusCode == 200)
545 		return B_OK;
546 
547 	fprintf(stderr, "failed to get screenshot from '%s': %" B_PRIi32 "\n",
548 		url.UrlString().String(), statusCode);
549 	return B_ERROR;
550 }
551 
552 
553 status_t
554 WebAppInterface::RequestCaptcha(BMessage& message)
555 {
556 	BString jsonString = JsonBuilder()
557 		.AddValue("jsonrpc", "2.0")
558 		.AddValue("id", ++fRequestIndex)
559 		.AddValue("method", "generateCaptcha")
560 		.AddArray("params")
561 			.AddObject()
562 			.EndObject()
563 		.EndArray()
564 	.End();
565 
566 	return _SendJsonRequest("captcha", jsonString, 0, message);
567 }
568 
569 
570 status_t
571 WebAppInterface::CreateUser(const BString& nickName,
572 	const BString& passwordClear, const BString& email,
573 	const BString& captchaToken, const BString& captchaResponse,
574 	const BString& languageCode, BMessage& message)
575 {
576 	JsonBuilder builder;
577 	builder
578 		.AddValue("jsonrpc", "2.0")
579 		.AddValue("id", ++fRequestIndex)
580 		.AddValue("method", "createUser")
581 		.AddArray("params")
582 			.AddObject()
583 				.AddValue("nickname", nickName)
584 				.AddValue("passwordClear", passwordClear);
585 
586 				if (!email.IsEmpty())
587 					builder.AddValue("email", email);
588 
589 				builder.AddValue("captchaToken", captchaToken)
590 				.AddValue("captchaResponse", captchaResponse)
591 				.AddValue("naturalLanguageCode", languageCode)
592 			.EndObject()
593 		.EndArray()
594 	;
595 
596 	BString jsonString = builder.End();
597 
598 	return _SendJsonRequest("user", jsonString, 0, message);
599 }
600 
601 
602 status_t
603 WebAppInterface::AuthenticateUser(const BString& nickName,
604 	const BString& passwordClear, BMessage& message)
605 {
606 	BString jsonString = JsonBuilder()
607 		.AddValue("jsonrpc", "2.0")
608 		.AddValue("id", ++fRequestIndex)
609 		.AddValue("method", "authenticateUser")
610 		.AddArray("params")
611 			.AddObject()
612 				.AddValue("nickname", nickName)
613 				.AddValue("passwordClear", passwordClear)
614 			.EndObject()
615 		.EndArray()
616 	.End();
617 
618 	return _SendJsonRequest("user", jsonString, 0, message);
619 }
620 
621 
622 // #pragma mark - private
623 
624 
625 status_t
626 WebAppInterface::_SendJsonRequest(const char* domain, BString jsonString,
627 	uint32 flags, BMessage& reply) const
628 {
629 	if (ServerSettings::UrlConnectionTraceLoggingEnabled())
630 		printf("_SendJsonRequest(%s)\n", jsonString.String());
631 
632 	BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v1/") << domain);
633 	bool isSecure = url.Protocol() == "https";
634 
635 	ProtocolListener listener(
636 		ServerSettings::UrlConnectionTraceLoggingEnabled());
637 	BUrlContext context;
638 
639 	BHttpHeaders headers;
640 	headers.AddHeader("Content-Type", "application/json");
641 	ServerSettings::AugmentHeaders(headers);
642 
643 	BHttpRequest request(url, isSecure, "HTTP", &listener, &context);
644 	request.SetMethod(B_HTTP_POST);
645 	request.SetHeaders(headers);
646 
647 	// Authentication via Basic Authentication
648 	// The other way would be to obtain a token and then use the Token Bearer
649 	// header.
650 	if ((flags & NEEDS_AUTHORIZATION) != 0
651 		&& !fUsername.IsEmpty() && !fPassword.IsEmpty()) {
652 		BHttpAuthentication authentication(fUsername, fPassword);
653 		authentication.SetMethod(B_HTTP_AUTHENTICATION_BASIC);
654 		context.AddAuthentication(url, authentication);
655 	}
656 
657 	BMemoryIO* data = new BMemoryIO(
658 		jsonString.String(), jsonString.Length() - 1);
659 
660 	request.AdoptInputData(data, jsonString.Length() - 1);
661 
662 	BMallocIO replyData;
663 	listener.SetDownloadIO(&replyData);
664 
665 	thread_id thread = request.Run();
666 	wait_for_thread(thread, NULL);
667 
668 	const BHttpResult& result = dynamic_cast<const BHttpResult&>(
669 		request.Result());
670 
671 	int32 statusCode = result.StatusCode();
672 	if (statusCode != 200) {
673 		printf("Response code: %" B_PRId32 "\n", statusCode);
674 		return B_ERROR;
675 	}
676 
677 	jsonString.SetTo(static_cast<const char*>(replyData.Buffer()),
678 		replyData.BufferLength());
679 	if (jsonString.Length() == 0)
680 		return B_ERROR;
681 
682 	status_t status = BJson::Parse(jsonString, reply);
683 	if (ServerSettings::UrlConnectionTraceLoggingEnabled() &&
684 		status == B_BAD_DATA) {
685 		printf("Parser choked on JSON:\n%s\n", jsonString.String());
686 	}
687 	return status;
688 }
689