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