Pool Video Switch v2
Software video switch for distributed remote display in a lecture environment
serverconnection.cpp
Go to the documentation of this file.
1 #include "serverconnection.h"
2 
3 #include "../vnc/vncserver.h"
4 #include "../../shared/util.h"
5 #include "../../shared/settings.h"
6 #include "../util/platform/blankscreen.h"
7 #include "../clientapp/clientapp.h"
8 
9 #include <QPixmap>
10 #include <QGuiApplication>
11 #include <QHostInfo>
12 #include <QScreen>
13 // For getting logged-in username
14 #include <sys/types.h>
15 #include <pwd.h>
16 #include <unistd.h>
17 
18 #define CHALLENGE_LEN 20
19 
20 ServerConnection::ServerConnection(const QString& host, const quint16 port, const QByteArray& sessionName, const QByteArray& certHash, bool autoConnect) :
21  QObject(nullptr), _timerDelete(0), _jpegQuality(80), _authed(0), _autoConnect(autoConnect), _isLocalConnection(-1), _sessionName(sessionName), _certHash(certHash)
22 {
23  _socket = new QSslSocket();
24  _blank = new BlankScreen();
25  connect(_socket, &QSslSocket::encrypted, this, &ServerConnection::sock_connected);
26  connect(_socket, &QSslSocket::readyRead, this, &ServerConnection::sock_dataArrival);
27  connect(_socket, &QSslSocket::disconnected, this, &ServerConnection::sock_closed);
28  connect(_socket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::errorOccurred),
30  connect(_socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
32  );
33  qDebug("Connecting to %s on port %d", host.toUtf8().data(), int(port));
34  _socket->ignoreSslErrors();
35  _socket->connectToHostEncrypted(host, port);
36  _timerId = startTimer(4000);
37  _lastData = QDateTime::currentMSecsSinceEpoch() + PING_TIMEOUT_MS;
38  _timerConnectionCheck = startTimer(5000);
39  // Connect the vnc start/stop signal to this class, so we can tell the server about successful vnc server startup
41 }
42 
44 {
45  this->disconnectFromServer();
46  if (_socket != nullptr) {
47  qCritical("**** SOCKET DELETE IN DESTRUCTOR");
48  _socket->deleteLater();
49  }
50  qDebug("*** Server connection destroyed.");
51  _blank->deleteLater();
52  _blank = nullptr;
53 }
54 
59 {
60  if (_socket == nullptr || _socket->state() != QAbstractSocket::ConnectedState)
61  return;
62  message.writeMessage(_socket);
63  if (!message.writeComplete()) {
64  qCritical() << "SendMessage to server failed!";
65  }
66 }
67 
69 {
70  NetworkMessage msg;
71  msg.setField(_ID, _ATTENTION);
72  msg.setField(_ENABLE, _BOOL(on));
73  sendMessage(msg);
74 }
75 
82 {
83  if (_timerDelete == 0) {
85  emit closeVnc();
86  emit disconnected(this);
87  _timerDelete = startTimer(500);
88  qDebug("Closing connection to server");
89  if (_socket != nullptr) {
90  _socket->blockSignals(true);
91  _socket->abort();
92  }
93  }
94 }
95 
102 {
103  _lastData = QDateTime::currentMSecsSinceEpoch() + PING_TIMEOUT_MS;
104  const QString &id = _fromServer.getFieldString(_ID);
105 
106  if (id == _ERROR) {
107  qWarning() << "Server sent error message:" << _fromServer.getFieldString(_ERROR);
108  return;
109  }
110 
111  if (_authed == 0) {
112  if (id == _CHALLENGE) {
113  // Initial challenge request by server
115  _myChallenge.resize(CHALLENGE_LEN);
116  for (int i = 0; i < CHALLENGE_LEN; ++i) {
117  _myChallenge[i] = char(slxrand() & 0xff);
118  }
119  QByteArray serverChallenge(_fromServer.getFieldBytes(_CHALLENGE));
120  _toServer.reset();
121  _toServer.setField(_ID, _CHALLENGE);
122  _toServer.setField(_HASH, genSha1(&_sessionName, &serverChallenge));
123  _toServer.setField(_CHALLENGE, _myChallenge);
125  qDebug("Received challenge, replying, sending own challenge step <- 1");
126  _authed = 1;
127  }
128  return;
129  }
130 
131  if (_authed == 1) {
132  if (id == _CHALLENGE) {
133  qDebug("Received challenge reply");
134  if (_timerId != 0) {
135  killTimer(_timerId);
136  _timerId = 0;
137  }
138  // Check challenge response
139  QByteArray serverhash(_fromServer.getFieldBytes(_HASH));
140  if (serverhash != genSha1(&_sessionName, &_myChallenge)
141  && !_autoConnect) {
142  qDebug("invalid. STOP.");
144  this->disconnectFromServer();
145  return;
146  }
148  const char *user = getpwuid(getuid())->pw_name;
149  if (user == nullptr || *user == '\0')
150  user = getenv("USER");
151  if (user == nullptr || *user == '\0')
152  user = getenv("USERNAME");
153  if (user == nullptr || *user == '\0')
154  user = "Hans Affe";
155  _toServer.reset();
156  _toServer.setField(_ID, _LOGIN);
157  _toServer.setField("HOST", QHostInfo::localHostName());
158  _toServer.setField("NAME", QString(user));
159  qDebug() << "logging into manager, exam mode is " << clientApp->isExamMode();
160  _toServer.setField(_EXAMMODE, clientApp->isExamMode() ? __TRUE : __FALSE);
161  /* TODO: (Question) Why is this here not using sendMessage() ? */
162  qDebug("Sending login request!");
164  _authed = 2;
165  qDebug("valid, step <- 2");
166  } else {
167  this->disconnectFromServer();
168  }
169  }
170  return;
171  }
172 
173  if (_authed == 2) {
174  if (id == _LOGIN) {
175  qDebug("login accepted, step <- 3");
176  _authed = 3;
178  }
179  return;
180  }
181 
182  // message THUMB - server requests screenshot as thumbnail
183  if (id == _THUMB) {
184  if (clientApp->isExamMode()) {
185  QByteArray emptyArray;
186  _toServer.setField(_ID, _THUMB);
187  _toServer.setField(_IMG, emptyArray);
189  return;
190  }
191  int x = _fromServer.getFieldString(_X).toInt();
192  int y = _fromServer.getFieldString(_Y).toInt();
193 
194  QRect primaryRect = QGuiApplication::primaryScreen()->geometry();
195  // Limit requested size so it won't easily leak sensitive information
196  if (x < 32) {
197  x = 32;
198  } else if (x > primaryRect.width() / 8) {
199  x = primaryRect.width() / 8;
200  }
201  if (y < 18) {
202  y = 18;
203  } else if (y > primaryRect.height() / 8) {
204  y = primaryRect.height() / 8;
205  }
206 
207  QPixmap desktop(
208  QGuiApplication::primaryScreen()->grabWindow(0,
209  primaryRect.x(), primaryRect.y(), primaryRect.width(), primaryRect.height()
210  ).scaled(x, y, Qt::KeepAspectRatio, Qt::SmoothTransformation
211  ));
212  QByteArray bytes;
213  QBuffer jpgBuffer(&bytes);
214  jpgBuffer.open(QIODevice::WriteOnly);
215  if (desktop.save(&jpgBuffer, "JPG", _jpegQuality)) { // writes pixmap into bytes in JPG format
216  // Try to adjust quality so we stay between 3 and 4.5 KB
217  if (_jpegQuality < 90 && bytes.size() < 3000)
218  _jpegQuality += 7;
219  else if (_jpegQuality > 40 && bytes.size() > 4500)
220  _jpegQuality -= 7;
221  } else {
222  // FALLBACK
223  bytes.clear();
224  QBuffer pngBuffer(&bytes);
225  pngBuffer.open(QIODevice::WriteOnly);
226  if (!desktop.save(&pngBuffer, "PNG")) { // writes pixmap into bytes in PNG format
227  qDebug("Could not convert screenshot to PNG nor JPG");
228  return; // Failed :-(
229  }
230  }
231  _toServer.reset();
232  _toServer.setField(_ID, _THUMB);
233  _toServer.setField(_IMG, bytes);
235  } // message VNCSERVER - start local vncserver
236  else if (id == _VNCSERVER) {
237  if (clientApp->isExamMode()) {
238  qDebug() << "denied request for vnc server (exam mode)";
239  return;
240  }
241  const bool enable = (_fromServer.getFieldString("ENABLE").toInt() != 0);
242  if (enable) {
243  emit closeVnc(); // In case we are watching some other client, stop doing so
245  } else {
247  }
248  } else if (id == _VNCCLIENT) {
249  if (clientApp->isExamMode()) {
250  qDebug() << "denied request for vnc projection (exam mode)";
251  return;
252  }
253  const QString host(_fromServer.getFieldString("HOST"));
254  const int port = _fromServer.getFieldString("PORT").toInt();
255  if (host.isEmpty() || port <= 0) {
256  emit closeVnc();
257  } else {
258  emit openVnc(host, port, _fromServer.getFieldString("ROPASS"), true, true, _fromServer.getFieldString("CAPTION"), _fromServer.getFieldString("CLIENTID").toInt(), _fromServer.getFieldBytes(_THUMB));
259  }
260  } else if (id == _LOCK) {
261  const bool enable = (_fromServer.getFieldString("ENABLE").toInt() != 0);
262  if (enable && !clientApp->isConnectedToLocalManager()) {
263  _blank->lock(_fromServer.getFieldString("MESSAGE"));
264  } else {
265  _blank->unlock();
266  }
267  } else if (id == _ATTENTION) {
269  }
270 }
271 
273 {
274  if (_socket == nullptr) {
275  return;
276  }
277  if (_socket->peerAddress() == _socket->localAddress()) {
278  _isLocalConnection = 1;
279  } else {
280  _isLocalConnection = 0;
281  }
282 }
283 
284 /*
285  * Override
286  */
287 
288 void ServerConnection::timerEvent(QTimerEvent *event)
289 {
290  if (event->timerId() == _timerConnectionCheck) {
291  if (_lastData < QDateTime::currentMSecsSinceEpoch()) {
292  qDebug() << "Ping timeout";
293  this->disconnectFromServer();
294  killTimer(_timerConnectionCheck);
295  }
296  } else if (event->timerId() == _timerId) {
297  qDebug() << "Connect timeout";
298  killTimer(_timerId);
299  _timerId = 0;
300  this->disconnectFromServer();
301  } else if (event->timerId() == _timerDelete) {
302  if (_socket == nullptr || _socket->state() == QAbstractSocket::UnconnectedState) {
303  if (_socket != nullptr)
304  _socket->deleteLater();
305  _socket = nullptr;
306  killTimer(_timerDelete);
307  this->deleteLater();
308  return;
309  }
310  _socket->abort();
311  qDebug("A socket is still pending...");
312  } else
313  killTimer(event->timerId());
314 }
315 
316 /*
317  * Slots
318  */
319 
325 void ServerConnection::onVncServerStartStop(int port, const QString &ropass, const QString &rwpass)
326 {
327  _toServer.reset();
328  _toServer.setField(_ID, _VNCSERVER);
329  if (port <= 0) {
330  _toServer.setField("PORT", QByteArray("0"));
331  } else {
332  _toServer.setField("PORT", QString::number(port));
333  _toServer.setField("ROPASS", ropass);
334  _toServer.setField("RWPASS", rwpass);
335  }
337 }
338 
343 void ServerConnection::onVncViewerStartStop(const bool started, const int clientId)
344 {
345  _toServer.reset();
346  _toServer.setField(_ID, _VNCCLIENT);
347  if (started)
348  _toServer.setField("ENABLED", __TRUE);
349  else
350  _toServer.setField("ENABLED", __FALSE);
351  _toServer.setField("CLIENTID", QString::number(clientId));
353 }
354 
359 void ServerConnection::sslErrors(const QList<QSslError> & errors)
360 {
361  _socket->ignoreSslErrors();
362  for (const auto &err : errors) {
363  qDebug("Connect SSL: %s", qPrintable(err.errorString()));
364  if (err.error() == QSslError::HostNameMismatch)
365  continue; // We don't pay attention to hostnames for validation
366  if (err.error() == QSslError::SelfSignedCertificate)
367  continue; // Also, this will always be the case; we check the fingerprint later
368  if (err.error() == QSslError::CertificateNotYetValid || err.error() == QSslError::CertificateExpired)
369  continue;
370  // Some other SSL error - do not ignore
371  qDebug() << "FATAL!";
372  _socket->ignoreSslErrors(QList<QSslError>());
373  return;
374  }
375 }
376 
378 {
379  if (_socket == nullptr || _socket->state() != QAbstractSocket::ConnectedState) {
380  qDebug("dataArrival called in bad state");
381  return;
382  }
383 
384  while (_socket->bytesAvailable() > 0) {
385  int retval = _fromServer.readMessage(_socket); // let the message read data from socket
386  if (retval == NM_READ_FAILED) { // error parsing msg, disconnect client!
387  this->disconnectFromServer();
388  return;
389  }
390  if (retval == NM_READ_INCOMPLETE)
391  return;
392  if (_fromServer.readComplete()) { // message is complete
393  this->handleMsg();
394  if (_socket == nullptr)
395  return;
396  _fromServer.reset();
397  }
398  }
399 }
400 
402 {
403  // should this be unreliable in some way i suggest using the signal "stateChanged()" instead
404  // and check if the state changed to unconnected.
405  qDebug("Socket was closed... oh well..");
406  this->disconnectFromServer();
407 }
408 
409 void ServerConnection::sock_error(QAbstractSocket::SocketError errcode)
410 {
411  qDebug("Connection error: %d", int(errcode));
412  this->disconnectFromServer();
413 }
414 
416 {
417  qDebug() << "Connection to server established and encrypted";
418  QByteArray cert(_socket->peerCertificate().digest(QCryptographicHash::Sha1));
419  if (_certHash != cert) {
421  this->disconnectFromServer();
422  return;
423  }
425 }
static const QByteArray __TRUE("1")
static const QByteArray __FALSE("0")
void stop()
VncServer::stop.
Definition: vncserver.cpp:139
void started(int port, const QString &ropass, const QString &rwpass)
QByteArray _sessionName
#define NM_READ_INCOMPLETE
QSslSocket * _socket
bool writeMessage(QAbstractSocket *socket)
QString getFieldString(const QByteArray &key) const
~ServerConnection() override
bool readComplete() const
void stateChange(ConnectWindow::ConnectionState state)
void sendMessage(NetworkMessage &message)
Send the given message to the server.
BlankScreen * _blank
NetworkMessage _toServer
#define PING_TIMEOUT_MS
Definition: settings.h:12
QByteArray genSha1(const QByteArray *a, const QByteArray *b, const QByteArray *c, const QByteArray *d, const QByteArray *e)
Definition: util.cpp:13
void disconnectFromServer()
Disconnect from current server.
void attentionChanged(bool state)
NetworkMessage _fromServer
void disconnected(ServerConnection *connection)
void onVncServerStartStop(int port, const QString &ropass, const QString &rwpass)
This slot is triggered by the vnc server runner once the external VNC server was succesfully started...
#define slxrand()
Definition: util.h:19
#define CHALLENGE_LEN
bool unlock()
Definition: blankscreen.cpp:68
void timerEvent(QTimerEvent *event) override
void start()
VncServer::start.
Definition: vncserver.cpp:77
QByteArray _certHash
bool lock(const QString &message)
Definition: blankscreen.cpp:40
void sock_error(QAbstractSocket::SocketError errcode)
int readMessage(QAbstractSocket *socket)
void sslErrors(const QList< QSslError > &errors)
An ssl error happened.
bool writeComplete() const
ServerConnection(const QString &host, quint16 port, const QByteArray &sessionName, const QByteArray &certHash, bool autoConnect)
#define clientApp
Definition: clientapp.h:12
void openVnc(const QString &host, int port, const QString &passwd, bool ro, bool fullscreen, const QString &caption, const int clientId, const QByteArray &rawThumb)
#define NM_READ_FAILED
static const QByteArray & _BOOL(bool val)
void handleMsg()
Handles an incoming message by the server.
QByteArray getFieldBytes(const QByteArray &key) const
void setField(const QByteArray &key, const QByteArray &value)
void onVncViewerStartStop(bool started, int clientId)
This slot is triggered once the internal VNC viewer has started or stopped displaying a VNC stream...
QByteArray _myChallenge
static VncServer * instance()
VncServer::instance.
Definition: vncserver.cpp:25
void sendAttention(bool on)