1 /*
2    Drawpile - a collaborative drawing program.
3 
4    Copyright (C) 2008-2019 Calle Laakkonen
5 
6    Drawpile is free software: you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation, either version 3 of the License, or
9    (at your option) any later version.
10 
11    Drawpile is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15 
16    You should have received a copy of the GNU General Public License
17    along with Drawpile.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "multiserver.h"
21 #include "initsys.h"
22 #include "database.h"
23 #include "templatefiles.h"
24 
25 #include "../libserver/session.h"
26 #include "../libserver/sessionserver.h"
27 #include "../libserver/thinserverclient.h"
28 #include "../libserver/serverconfig.h"
29 #include "../libserver/serverlog.h"
30 #include "../libserver/sslserver.h"
31 #include "../libshared/util/whatismyip.h"
32 
33 #include <QTcpSocket>
34 #include <QFileInfo>
35 #include <QDateTime>
36 #include <QDir>
37 #include <QJsonObject>
38 #include <QJsonArray>
39 #include <QRegularExpression>
40 
41 namespace server {
42 
MultiServer(ServerConfig * config,QObject * parent)43 MultiServer::MultiServer(ServerConfig *config, QObject *parent)
44 	: QObject(parent),
45 	m_config(config),
46 	m_server(nullptr),
47 	m_state(STOPPED),
48 	m_autoStop(false),
49 	m_port(0)
50 {
51 	m_sessions = new SessionServer(config, this);
52 	m_started = QDateTime::currentDateTimeUtc();
53 
54 	connect(m_sessions, &SessionServer::sessionCreated, this, &MultiServer::assignRecording);
55 	connect(m_sessions, &SessionServer::sessionEnded, this, &MultiServer::tryAutoStop);
56 	connect(m_sessions, &SessionServer::userCountChanged, [this](int users) {
57 		printStatusUpdate();
58 		emit userCountChanged(users);
59 		// The server will be fully stopped after all users have disconnected
60 		if(users == 0) {
61 			if(m_state == STOPPING)
62 				stop();
63 			else
64 				tryAutoStop();
65 		}
66 	});
67 }
68 
69 #ifndef NDEBUG
setRandomLag(uint lag)70 void MultiServer::setRandomLag(uint lag)
71 {
72 	m_sessions->setRandomLag(lag);
73 }
74 #endif
75 
76 /**
77  * @brief Automatically stop server when last session is closed
78  *
79  * This is used in socket activation mode. The server will be restarted
80  * by the system init daemon when needed again.
81  * @param autostop
82  */
setAutoStop(bool autostop)83 void MultiServer::setAutoStop(bool autostop)
84 {
85 	m_autoStop = autostop;
86 }
87 
setRecordingPath(const QString & path)88 void MultiServer::setRecordingPath(const QString &path)
89 {
90 	m_recordingPath = path;
91 }
92 
setSessionDirectory(const QDir & path)93 void MultiServer::setSessionDirectory(const QDir &path)
94 {
95 	m_sessions->setSessionDir(path);
96 }
97 
setTemplateDirectory(const QDir & dir)98 void MultiServer::setTemplateDirectory(const QDir &dir)
99 {
100 	const TemplateLoader *old = m_sessions->templateLoader();
101 	TemplateFiles *loader = new TemplateFiles(dir, m_sessions);
102 	m_sessions->setTemplateLoader(loader);
103 	delete old;
104 }
105 
createServer()106 bool MultiServer::createServer()
107 {
108 	if(!m_sslCertFile.isEmpty() && !m_sslKeyFile.isEmpty()) {
109 		SslServer *server = new SslServer(m_sslCertFile, m_sslKeyFile, this);
110 		if(!server->isValidCert()) {
111 			emit serverStartError("Couldn't load TLS certificate");
112 			return false;
113 		}
114 		m_server = server;
115 
116 	} else {
117 		m_server = new QTcpServer(this);
118 	}
119 
120 	connect(m_server, &QTcpServer::newConnection, this, &MultiServer::newClient);
121 
122 	return true;
123 }
124 
125 /**
126  * @brief Start listening on the specified address.
127  * @param port the port to listen on
128  * @param address listening address
129  * @return true on success
130  */
start(quint16 port,const QHostAddress & address)131 bool MultiServer::start(quint16 port, const QHostAddress& address) {
132 	Q_ASSERT(m_state == STOPPED);
133 	m_state = RUNNING;
134 	if(!createServer()) {
135 		delete m_server;
136 		m_server = nullptr;
137 		m_state = STOPPED;
138 		return false;
139 	}
140 
141 	if(!m_server->listen(address, port)) {
142 		emit serverStartError(m_server->errorString());
143 		m_sessions->config()->logger()->logMessage(Log().about(Log::Level::Error, Log::Topic::Status).message(m_server->errorString()));
144 		delete m_server;
145 		m_server = nullptr;
146 		m_state = STOPPED;
147 		return false;
148 	}
149 
150 	m_port = m_server->serverPort();
151 
152 	InternalConfig icfg = m_config->internalConfig();
153 	icfg.realPort = m_port;
154 	m_config->setInternalConfig(icfg);
155 
156 	emit serverStarted();
157 	m_sessions->config()->logger()->logMessage(Log().about(Log::Level::Info, Log::Topic::Status)
158 		.message(QString("Started listening on port %1 at address %2").arg(port).arg(address.toString())));
159 	return true;
160 }
161 
162 /**
163  * @brief Start listening on the given file descriptor
164  * @param fd
165  * @return true on success
166  */
startFd(int fd)167 bool MultiServer::startFd(int fd)
168 {
169 	Q_ASSERT(m_state == STOPPED);
170 	m_state = RUNNING;
171 	if(!createServer())
172 		return false;
173 
174 	if(!m_server->setSocketDescriptor(fd)) {
175 		m_sessions->config()->logger()->logMessage(Log().about(Log::Level::Error, Log::Topic::Status).message("Couldn't set server socket descriptor!"));
176 		delete m_server;
177 		m_server = nullptr;
178 		m_state = STOPPED;
179 		return false;
180 	}
181 
182 	m_port = m_server->serverPort();
183 
184 	m_sessions->config()->logger()->logMessage(Log().about(Log::Level::Info, Log::Topic::Status)
185 		.message(QString("Started listening on passed socket")));
186 
187 	return true;
188 }
189 
190 /**
191  * @brief Assign a recording file name to a new session
192  *
193  * The name is generated by replacing placeholders in the file name pattern.
194  * If a file with the same name exists, a number is inserted just before the suffix.
195  *
196  * If the file name pattern points to a directory, the default pattern "%d %t session %i.dprec"
197  * will be used.
198  *
199  * The following placeholders are supported:
200  *
201  *  ~/ - user's home directory (at the start of the pattern)
202  *  %d - the current date (YYYY-MM-DD)
203  *  %h - the current time (HH.MM.SS)
204  *  %i - session ID
205  *  %a - session alias (or ID if not assigned)
206  *
207  * @param session
208  */
assignRecording(Session * session)209 void MultiServer::assignRecording(Session *session)
210 {
211 	QString filename = m_recordingPath;
212 
213 	if(filename.isEmpty())
214 		return;
215 
216 	// Expand home directory
217 	if(filename.startsWith("~/")) {
218 		filename = QString(qgetenv("HOME")) + filename.mid(1);
219 	}
220 
221 	// Use default file pattern if target is a directory
222 	QFileInfo fi(filename);
223 	if(fi.isDir()) {
224 		filename = QFileInfo(QDir(filename), "%d %t session %i.dprec").absoluteFilePath();
225 	}
226 
227 	// Expand placeholders
228 	QDateTime now = QDateTime::currentDateTime();
229 	filename.replace("%d", now.toString("yyyy-MM-dd"));
230 	filename.replace("%t", now.toString("HH.mm.ss"));
231 	filename.replace("%i", session->id());
232 	filename.replace("%a", session->aliasOrId());
233 
234 	fi = filename;
235 
236 	if(!fi.absoluteDir().mkpath(".")) {
237 		qWarning("Recording directory \"%s\" does not exist and cannot be created!", qPrintable(fi.absolutePath()));
238 	} else {
239 		session->setRecordingFile(fi.absoluteFilePath());
240 	}
241 }
242 
243 /**
244  * @brief Accept or reject new client connection
245  */
newClient()246 void MultiServer::newClient()
247 {
248 	QTcpSocket *socket = m_server->nextPendingConnection();
249 
250 	m_sessions->config()->logger()->logMessage(Log().about(Log::Level::Info, Log::Topic::Status)
251 		.user(0, socket->peerAddress(), QString())
252 		.message(QStringLiteral("New client connected")));
253 
254 	auto *client = new ThinServerClient(socket, m_sessions->config()->logger());
255 
256 	if(m_config->isAddressBanned(socket->peerAddress())) {
257 		client->log(Log().about(Log::Level::Warn, Log::Topic::Kick)
258 			.user(0, socket->peerAddress(), QString())
259 			.message("Kicking banned user straight away"));
260 
261 		client->disconnectClient(Client::DisconnectionReason::Error, "BANNED");
262 
263 	} else {
264 		m_sessions->addClient(client);
265 		printStatusUpdate();
266 	}
267 }
268 
269 
printStatusUpdate()270 void MultiServer::printStatusUpdate()
271 {
272 	initsys::notifyStatus(QString("%1 users and %2 sessions")
273 		.arg(m_sessions->totalUsers())
274 		.arg(m_sessions->sessionCount())
275 	);
276 }
277 
278 /**
279  * @brief Stop the server if vacant (and autostop is enabled)
280  */
tryAutoStop()281 void MultiServer::tryAutoStop()
282 {
283 	if(m_state == RUNNING && m_autoStop && m_sessions->sessionCount() == 0 && m_sessions->totalUsers() == 0) {
284 		m_sessions->config()->logger()->logMessage(Log()
285 			.about(Log::Level::Info, Log::Topic::Status)
286 			.message("Autostopping due to lack of sessions."));
287 		stop();
288 	}
289 }
290 
291 /**
292  * Disconnect all clients and stop listening.
293  */
stop()294 void MultiServer::stop() {
295 	if(m_state == RUNNING) {
296 		m_sessions->config()->logger()->logMessage(Log()
297 			.about(Log::Level::Info, Log::Topic::Status)
298 			.message(QString("Stopping server and kicking out %1 users...")
299 					 .arg(m_sessions->totalUsers())
300 			));
301 
302 		m_state = STOPPING;
303 		m_server->close();
304 		m_port = 0;
305 
306 		m_sessions->stopAll();
307 	}
308 
309 	if(m_state == STOPPING) {
310 		if(m_sessions->totalUsers() == 0) {
311 			m_state = STOPPED;
312 			delete m_server;
313 			m_server = nullptr;
314 			m_sessions->config()->logger()->logMessage(Log()
315 				.about(Log::Level::Info, Log::Topic::Status)
316 				.message("Server stopped."));
317 			emit serverStopped();
318 		}
319 	}
320 }
321 
callJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)322 JsonApiResult MultiServer::callJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
323 {
324 	QString head;
325 	QStringList tail;
326 	std::tie(head, tail) = popApiPath(path);
327 
328 	if(head == "server")
329 		return serverJsonApi(method, tail, request);
330 	else if(head == "status")
331 		return statusJsonApi(method, tail, request);
332 	else if(head == "sessions")
333 		return m_sessions->callSessionJsonApi(method, tail, request);
334 	else if(head == "users")
335 		return m_sessions->callUserJsonApi(method, tail, request);
336 	else if(head == "banlist")
337 		return banlistJsonApi(method, tail, request);
338 	else if(head == "listserverwhitelist")
339 		return listserverWhitelistJsonApi(method, tail, request);
340 	else if(head == "accounts")
341 		return accountsJsonApi(method, tail, request);
342 	else if(head == "log")
343 		return logJsonApi(method, tail, request);
344 
345 	return JsonApiNotFound();
346 }
347 
callJsonApiAsync(const QString & requestId,JsonApiMethod method,const QStringList & path,const QJsonObject & request)348 void MultiServer::callJsonApiAsync(const QString &requestId, JsonApiMethod method, const QStringList &path, const QJsonObject &request)
349 {
350 	JsonApiResult result = callJsonApi(method, path, request);
351 	emit jsonApiResult(requestId, result);
352 }
353 
354 /**
355  * @brief Serverwide settings
356  *
357  * @param method
358  * @param path
359  * @param request
360  * @return
361  */
serverJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)362 JsonApiResult MultiServer::serverJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
363 {
364 	if(!path.isEmpty())
365 		return JsonApiNotFound();
366 
367 	if(method != JsonApiMethod::Get && method != JsonApiMethod::Update)
368 		return JsonApiBadMethod();
369 
370 	const ConfigKey settings[] = {
371 		config::ClientTimeout,
372 		config::SessionSizeLimit,
373 		config::AutoresetThreshold,
374 		config::SessionCountLimit,
375 		config::EnablePersistence,
376 		config::ArchiveMode,
377 		config::IdleTimeLimit,
378 		config::ServerTitle,
379 		config::WelcomeMessage,
380 		config::PrivateUserList,
381 		config::AllowGuestHosts,
382 		config::AllowGuests,
383 #ifdef HAVE_LIBSODIUM
384 		config::UseExtAuth,
385 		config::ExtAuthKey,
386 		config::ExtAuthGroup,
387 		config::ExtAuthFallback,
388 		config::ExtAuthMod,
389 		config::ExtAuthHost,
390 		config::ExtAuthAvatars,
391 #endif
392 		config::LogPurgeDays,
393 		config::AllowCustomAvatars,
394 		config::AbuseReport,
395 		config::ReportToken
396 	};
397 	const int settingCount = sizeof(settings) / sizeof(settings[0]);
398 
399 	if(method==JsonApiMethod::Update) {
400 		for(int i=0;i<settingCount;++i) {
401 			if(request.contains(settings[i].name)) {
402 				m_config->setConfigString(settings[i], request[settings[i].name].toVariant().toString());
403 			}
404 		}
405 	}
406 
407 	QJsonObject result;
408 	for(int i=0;i<settingCount;++i) {
409 		result[settings[i].name] = QJsonValue::fromVariant(m_config->getConfigVariant(settings[i]));
410 	}
411 
412 	// Hide values for disabled features
413 	if(!m_config->internalConfig().reportUrl.isValid())
414 		result.remove(config::AbuseReport.name);
415 
416 	if(!m_config->internalConfig().extAuthUrl.isValid())
417 		result.remove(config::UseExtAuth.name);
418 
419 	return JsonApiResult { JsonApiResult::Ok, QJsonDocument(result) };
420 }
421 
422 /**
423  * @brief Read only view of server status
424  *
425  * @param method
426  * @param path
427  * @param request
428  * @return
429  */
statusJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)430 JsonApiResult MultiServer::statusJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
431 {
432 	Q_UNUSED(request);
433 
434 	if(!path.isEmpty())
435 		return JsonApiNotFound();
436 
437 	if(method != JsonApiMethod::Get)
438 		return JsonApiBadMethod();
439 
440 	QJsonObject result;
441 	result["started"] = m_started.toString("yyyy-MM-dd HH:mm:ss");
442 	result["sessions"] = m_sessions->sessionCount();
443 	result["maxSessions"] = m_config->getConfigInt(config::SessionCountLimit);
444 	result["users"] = m_sessions->totalUsers();
445 	QString localhost = m_config->internalConfig().localHostname;
446 	if(localhost.isEmpty())
447 		localhost = WhatIsMyIp::guessLocalAddress();
448 	result["ext_host"] = localhost;
449 	result["ext_port"] = m_config->internalConfig().getAnnouncePort();
450 
451 	return JsonApiResult { JsonApiResult::Ok, QJsonDocument(result) };
452 }
453 
454 /**
455  * @brief View and modify the serverwide banlist
456  *
457  * @param method
458  * @param path
459  * @param request
460  * @return
461  */
banlistJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)462 JsonApiResult MultiServer::banlistJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
463 {
464 	// Database is needed to manipulate the banlist
465 	Database *db = qobject_cast<Database*>(m_config);
466 	if(!db)
467 		return JsonApiNotFound();
468 
469 	if(path.size()==1) {
470 		if(method != JsonApiMethod::Delete)
471 			return JsonApiBadMethod();
472 		if(db->deleteBan(path.at(0).toInt())) {
473 			QJsonObject body;
474 			body["status"] = "ok";
475 			body["deleted"] = path.at(0).toInt();
476 			return JsonApiResult {JsonApiResult::Ok, QJsonDocument(body)};
477 		} else
478 			return JsonApiNotFound();
479 	}
480 
481 	if(!path.isEmpty())
482 		return JsonApiNotFound();
483 
484 	if(method == JsonApiMethod::Get) {
485 		return JsonApiResult { JsonApiResult::Ok, QJsonDocument(db->getBanlist()) };
486 
487 	} else if(method == JsonApiMethod::Create) {
488 		QHostAddress ip { request["ip"].toString() };
489 		if(ip.isNull())
490 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Valid IP address required");
491 		int subnet = request["subnet"].toInt();
492 		QDateTime expiration = QDateTime::fromString(request["expires"].toString(), "yyyy-MM-dd HH:mm:ss");
493 		if(expiration.isNull())
494 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Valid expiration time required");
495 		QString comment = request["comment"].toString();
496 
497 		return JsonApiResult { JsonApiResult::Ok, QJsonDocument(db->addBan(ip, subnet, expiration, comment)) };
498 
499 	} else
500 		return JsonApiBadMethod();
501 }
502 
503 /**
504  * @brief View and modify the list server URL whitelist
505  *
506  * @param method
507  * @param path
508  * @param request
509  * @return
510  */
listserverWhitelistJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)511 JsonApiResult MultiServer::listserverWhitelistJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
512 {
513 	// Database is needed to manipulate the whitelist
514 	Database *db = qobject_cast<Database*>(m_config);
515 	if(!db)
516 		return JsonApiNotFound();
517 
518 	if(!path.isEmpty())
519 		return JsonApiNotFound();
520 
521 	if(method == JsonApiMethod::Update) {
522 		QStringList whitelist;
523 		for(const auto &v : request["whitelist"].toArray()) {
524 			const QString str = v.toString();
525 			if(str.isEmpty())
526 				continue;
527 
528 			const QRegularExpression re(str);
529 			if(!re.isValid())
530 				return JsonApiErrorResult(JsonApiResult::BadRequest, str + ": " + re.errorString());
531 			whitelist << str;
532 		}
533 		if(!request["enabled"].isUndefined())
534 			db->setConfigBool(config::AnnounceWhiteList, request["enabled"].toBool());
535 		if(!request["whitelist"].isUndefined())
536 			db->updateListServerWhitelist(whitelist);
537 	}
538 
539 	const QJsonObject o {
540 		{"enabled", db->getConfigBool(config::AnnounceWhiteList)},
541 		{"whitelist", QJsonArray::fromStringList(db->listServerWhitelist())}
542 	};
543 
544 	return JsonApiResult { JsonApiResult::Ok, QJsonDocument(o) };
545 }
546 
547 /**
548  * @brief View and modify registered user accounts
549  *
550  * @param method
551  * @param path
552  * @param request
553  * @return
554  */
accountsJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)555 JsonApiResult MultiServer::accountsJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
556 {
557 	// Database is needed to manipulate account list
558 	Database *db = qobject_cast<Database*>(m_config);
559 	if(!db)
560 		return JsonApiNotFound();
561 
562 	if(path.size()==1) {
563 		if(method == JsonApiMethod::Update) {
564 			QJsonObject o = db->updateAccount(path.at(0).toInt(), request);
565 			if(o.isEmpty())
566 				return JsonApiNotFound();
567 			return JsonApiResult {JsonApiResult::Ok, QJsonDocument(o)};
568 
569 		} else if(method == JsonApiMethod::Delete) {
570 			if(db->deleteAccount(path.at(0).toInt())) {
571 				QJsonObject body;
572 				body["status"] = "ok";
573 				body["deleted"] = path.at(0).toInt();
574 				return JsonApiResult {JsonApiResult::Ok, QJsonDocument(body)};
575 			} else {
576 				return JsonApiNotFound();
577 			}
578 		} else {
579 			return JsonApiBadMethod();
580 		}
581 	}
582 
583 	if(!path.isEmpty())
584 		return JsonApiNotFound();
585 
586 	if(method == JsonApiMethod::Get) {
587 		return JsonApiResult { JsonApiResult::Ok, QJsonDocument(db->getAccountList()) };
588 
589 	} else if(method == JsonApiMethod::Create) {
590 		QString username = request["username"].toString();
591 		QString password = request["password"].toString();
592 		bool locked = request["locked"].toBool();
593 		QString flags = request["flags"].toString();
594 		if(username.isEmpty())
595 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Username required");
596 		if(password.isEmpty())
597 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Password required");
598 
599 		QJsonObject o = db->addAccount(username, password, locked, flags.split(','));
600 		if(o.isEmpty())
601 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Error");
602 		return JsonApiResult { JsonApiResult::Ok, QJsonDocument(o) };
603 
604 	} else
605 		return JsonApiBadMethod();
606 }
607 
logJsonApi(JsonApiMethod method,const QStringList & path,const QJsonObject & request)608 JsonApiResult MultiServer::logJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request)
609 {
610 	if(!path.isEmpty())
611 		return JsonApiNotFound();
612 	if(method != JsonApiMethod::Get)
613 		return JsonApiBadMethod();
614 
615 	auto q = m_config->logger()->query();
616 	q.page(request.value("page").toInt(), 100);
617 
618 	if(request.contains("session"))
619 		q.session(request.value("session").toString());
620 
621 	if(request.contains("after")) {
622 		QDateTime after = QDateTime::fromString(request.value("after").toString(), Qt::ISODate);
623 		if(!after.isValid())
624 			return JsonApiErrorResult(JsonApiResult::BadRequest, "Invalid timestamp");
625 		q.after(after);
626 	}
627 
628 	QJsonArray out;
629 	for(const Log &log : q.get()) {
630 		out.append(log.toJson());
631 	}
632 
633 	return JsonApiResult { JsonApiResult::Ok, QJsonDocument(out) };
634 }
635 
636 }
637