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