qmlfrontend/net_session.cpp
author Simon McVittie <smcv@debian.org>
Mon, 12 Sep 2022 10:40:53 -0400
branch1.0.0
changeset 15859 7b1d6dfa3173
parent 15078 fb7a9b0119d3
child 15891 d52f5d8e75e6
permissions -rw-r--r--
Remove FindSDL2 find-module, use sdl2-config.cmake instead This requires SDL >= 2.0.4. Since <https://bugzilla.libsdl.org/show_bug.cgi?id=2464> was fixed in SDL 2.0.4, SDL behaves as a CMake "config-file package", even if it was not itself built using CMake: it installs a sdl2-config.cmake file to ${libdir}/cmake/SDL2, which tells CMake where to find SDL's headers and library, analogous to a pkg-config .pc file. As a result, we no longer need to copy/paste a "find-module package" to be able to find a system copy of SDL >= 2.0.4 with find_package(SDL2). Find-module packages are now discouraged by the CMake developers, in favour of having upstream projects behave as config-file packages. This results in a small API change: FindSDL2 used to set SDL2_INCLUDE_DIR and SDL2_LIBRARY, but the standard behaviour for config-file packages is to set <name>_INCLUDE_DIRS and <name>_LIBRARIES. Use the CONFIG keyword to make sure we search in config-file package mode, and will not find a FindSDL2.cmake in some other directory that implements the old interface. In addition to deleting redundant code, this avoids some assumptions in FindSDL2 about the layout of a SDL installation. The current libsdl2-dev package in Debian breaks those assumptions; this is considered a bug and will hopefully be fixed soon, but it illustrates how fragile these assumptions can be. We can be more robust against different installation layouts by relying on SDL's own CMake integration. When linking to a copy of CMake in a non-standard location, users can now set the SDL2_DIR or CMAKE_PREFIX_PATH environment variable to point to it; previously, these users would have used the SDL2DIR environment variable. This continues to be unnecessary if using matching system-wide installations of CMake and SDL2, for example both from Debian.

#include "net_session.h"

#include <QUuid>

#include "players_model.h"
#include "rooms_model.h"

NetSession::NetSession(QObject *parent)
    : QObject(parent),
      m_playersModel(new PlayersListModel()),
      m_roomsModel(new RoomsListModel()),
      m_sessionState(NotConnected) {}

NetSession::~NetSession() { close(); }

QUrl NetSession::url() const { return m_url; }

QAbstractSocket::SocketState NetSession::state() const {
  if (m_socket)
    return m_socket->state();
  else
    return QAbstractSocket::UnconnectedState;
}

void NetSession::open() {
  m_socket.reset(new QTcpSocket());

  connect(m_socket.data(), &QAbstractSocket::stateChanged, this,
          &NetSession::stateChanged);
  connect(m_socket.data(), &QTcpSocket::readyRead, this,
          &NetSession::onReadyRead);

  m_socket->connectToHost(m_url.host(),
                          static_cast<quint16>(m_url.port(46631)));
}

QString NetSession::nickname() const { return m_nickname; }

NetSession::SessionState NetSession::sessionState() const {
  return m_sessionState;
}

QString NetSession::room() const { return m_room; }

QString NetSession::passwordHash() const { return m_passwordHash; }

void NetSession::setUrl(const QUrl &url) {
  if (m_url == url) return;

  m_url = url;
  emit urlChanged(m_url);
}

void NetSession::setNickname(const QString &nickname) {
  if (m_nickname == nickname) return;

  m_nickname = nickname;
  emit nicknameChanged(m_nickname);
}

void NetSession::setPasswordHash(const QString &passwordHash) {
  if (m_passwordHash == passwordHash) return;

  m_passwordHash = passwordHash;
  emit passwordHashChanged(m_passwordHash);

  if (m_sessionState == Authentication) sendPassword();
}

void NetSession::setRoom(const QString &room) {
  if (m_room == room) return;

  m_room = room;
  emit roomChanged(m_room);
}

void NetSession::close() {
  if (!m_socket.isNull()) {
    m_socket->disconnectFromHost();
    m_socket.clear();

    setSessionState(NotConnected);
    setRoom({});
  }
}

void NetSession::onReadyRead() {
  while (m_socket->canReadLine()) {
    auto line = QString::fromUtf8(m_socket->readLine().simplified());

    if (line.isEmpty()) {
      parseNetMessage(m_buffer);
      m_buffer.clear();
    } else {
      m_buffer.append(line);
    }
  }
}

void NetSession::parseNetMessage(const QStringList &message) {
  if (message.isEmpty()) {
    qWarning() << "Empty net message received";
    return;
  }

  qDebug() << "[SERVER]" << message;

  using Handler = std::function<void(NetSession *, const QStringList &)>;
  static QMap<QString, Handler> commandsMap{
      {"ADD_TEAM", &NetSession::handleAddTeam},
      {"ASKPASSWORD", &NetSession::handleAskPassword},
      {"BANLIST", &NetSession::handleBanList},
      {"BYE", &NetSession::handleBye},
      {"CFG", &NetSession::handleCfg},
      {"CHAT", &NetSession::handleChat},
      {"CLIENT_FLAGS", &NetSession::handleClientFlags},
      {"CONNECTED", &NetSession::handleConnected},
      {"EM", &NetSession::handleEm},
      {"ERROR", &NetSession::handleError},
      {"HH_NUM", &NetSession::handleHhNum},
      {"INFO", &NetSession::handleInfo},
      {"JOINED", &NetSession::handleJoined},
      {"JOINING", &NetSession::handleJoining},
      {"KICKED", &NetSession::handleKicked},
      {"LEFT", &NetSession::handleLeft},
      {"LOBBY:JOINED", &NetSession::handleLobbyJoined},
      {"LOBBY:LEFT", &NetSession::handleLobbyLeft},
      {"NICK", &NetSession::handleNick},
      {"NOTICE", &NetSession::handleNotice},
      {"PING", &NetSession::handlePing},
      {"PONG", &NetSession::handlePong},
      {"PROTO", &NetSession::handleProto},
      {"REDIRECT", &NetSession::handleRedirect},
      {"REMOVE_TEAM", &NetSession::handleRemoveTeam},
      {"REPLAY_START", &NetSession::handleReplayStart},
      {"ROOMABANDONED", &NetSession::handleRoomAbandoned},
      {"ROOM", &NetSession::handleRoom},
      {"ROOMS", &NetSession::handleRooms},
      {"ROUND_FINISHED", &NetSession::handleRoundFinished},
      {"RUN_GAME", &NetSession::handleRunGame},
      {"SERVER_AUTH", &NetSession::handleServerAuth},
      {"SERVER_MESSAGE", &NetSession::handleServerMessage},
      {"SERVER_VARS", &NetSession::handleServerVars},
      {"TEAM_ACCEPTED", &NetSession::handleTeamAccepted},
      {"TEAM_COLOR", &NetSession::handleTeamColor},
      {"WARNING", &NetSession::handleWarning},
  };

  auto handler =
      commandsMap.value(message[0], &NetSession::handleUnknownCommand);

  handler(this, message.mid(1));
}

void NetSession::handleConnected(const QStringList &parameters) {
  if (parameters.length() < 2 || parameters[1].toInt() < cMinServerVersion) {
    send("QUIT", "Server too old");
    emit error(tr("Server too old"));
    close();
  } else {
    setSessionState(Login);

    send("NICK", m_nickname);
    send("PROTO", QString::number(cProtocolVersion));
  }
}

void NetSession::handlePing(const QStringList &parameters) {
  send("PONG", parameters);
}

void NetSession::handleBye(const QStringList &parameters) { close(); }

void NetSession::handleUnknownCommand(const QStringList &parameters) {
  Q_UNUSED(parameters);

  qWarning() << "Command is not recognized";
}

void NetSession::handleAddTeam(const QStringList &parameters) {}

void NetSession::handleAskPassword(const QStringList &parameters) {
  if (parameters.length() != 1 || parameters[0].length() < 16) {
    qWarning("Bad ASKPASSWORD message");
    return;
  }

  setSessionState(Authentication);

  m_serverSalt = parameters[0];
  m_clientSalt = QUuid::createUuid().toString();

  if (m_passwordHash.isEmpty()) {
    emit passwordAsked();
  } else {
    sendPassword();
  }
}

void NetSession::handleBanList(const QStringList &parameters) {}

void NetSession::handleCfg(const QStringList &parameters) {}

void NetSession::handleChat(const QStringList &parameters) {}

void NetSession::handleClientFlags(const QStringList &parameters) {}

void NetSession::handleEm(const QStringList &parameters) {}

void NetSession::handleError(const QStringList &parameters) {}

void NetSession::handleHhNum(const QStringList &parameters) {}

void NetSession::handleInfo(const QStringList &parameters) {}

void NetSession::handleJoined(const QStringList &parameters) {}

void NetSession::handleJoining(const QStringList &parameters) {}

void NetSession::handleKicked(const QStringList &parameters) {}

void NetSession::handleLeft(const QStringList &parameters) {}

void NetSession::handleLobbyJoined(const QStringList &parameters) {
  for (auto player : parameters) {
    if (player == m_nickname) {
      // check if server is authenticated or no authentication was performed at
      // all
      if (!m_serverAuthHash.isEmpty()) {
        emit error(tr("Server authentication error"));

        close();
      }

      setSessionState(Lobby);
    }

    m_playersModel->addPlayer(player, false);
  }
}

void NetSession::handleLobbyLeft(const QStringList &parameters) {
  if (parameters.length() == 1) {
    m_playersModel->removePlayer(parameters[0]);
  } else if (parameters.length() == 2) {
    m_playersModel->removePlayer(parameters[0], parameters[1]);
  } else {
    qWarning("Malformed LOBBY:LEFT message");
  }
}

void NetSession::handleNick(const QStringList &parameters) {
  if (parameters.length()) setNickname(parameters[0]);
}

void NetSession::handleNotice(const QStringList &parameters) {}

void NetSession::handlePong(const QStringList &parameters) {
  Q_UNUSED(parameters)
}

void NetSession::handleProto(const QStringList &parameters) {}

void NetSession::handleRedirect(const QStringList &parameters) {}

void NetSession::handleRemoveTeam(const QStringList &parameters) {}

void NetSession::handleReplayStart(const QStringList &parameters) {}

void NetSession::handleRoomAbandoned(const QStringList &parameters) {}

void NetSession::handleRoom(const QStringList &parameters) {
  if (parameters.size() == 10 && parameters[0] == "ADD") {
    m_roomsModel->addRoom(parameters.mid(1));
  } else if (parameters.length() == 11 && parameters[0] == "UPD") {
    m_roomsModel->updateRoom(parameters[1], parameters.mid(2));

    // keep track of room name so correct name is displayed
    if (m_room == parameters[1]) {
      setRoom(parameters[2]);
    }
  } else if (parameters.size() == 2 && parameters[0] == "DEL") {
    m_roomsModel->removeRoom(parameters[1]);
  }
}

void NetSession::handleRooms(const QStringList &parameters) {
  if (parameters.size() % 9) {
    qWarning("Net: Malformed ROOMS message");
    return;
  }

  m_roomsModel->setRoomsList(parameters);
}

void NetSession::handleRoundFinished(const QStringList &parameters) {}

void NetSession::handleRunGame(const QStringList &parameters) {}

void NetSession::handleServerAuth(const QStringList &parameters) {}

void NetSession::handleServerMessage(const QStringList &parameters) {}

void NetSession::handleServerVars(const QStringList &parameters) {}

void NetSession::handleTeamAccepted(const QStringList &parameters) {}

void NetSession::handleTeamColor(const QStringList &parameters) {}

void NetSession::handleWarning(const QStringList &parameters) {}

void NetSession::send(const QString &message) { send(QStringList(message)); }

void NetSession::send(const QString &message, const QString &param) {
  send(QStringList{message, param});
}

void NetSession::send(const QString &message, const QStringList &parameters) {
  send(QStringList(message) + parameters);
}

void NetSession::send(const QStringList &message) {
  Q_ASSERT(!m_socket.isNull());

  qDebug() << "[CLIENT]" << message;

  m_socket->write(message.join('\n').toUtf8() + "\n\n");
}

void NetSession::sendPassword() {
  /* When we got password hash, and server asked us for a password, perform
   * mutual authentication: at this point we have salt chosen by server. Client
   * sends client salt and hash of secret (password hash) salted with client
   * salt, server salt, and static salt (predefined string + protocol number).
   * Server should respond with hash of the same set in different order.
   */

  if (m_passwordHash.isEmpty() || m_serverSalt.isEmpty()) return;

  QString hash =
      QCryptographicHash::hash(m_clientSalt.toLatin1()
                                   .append(m_serverSalt.toLatin1())
                                   .append(m_passwordHash)
                                   .append(QByteArray::number(cProtocolVersion))
                                   .append("!hedgewars"),
                               QCryptographicHash::Sha1)
          .toHex();

  m_serverHash =
      QCryptographicHash::hash(m_serverSalt.toLatin1()
                                   .append(m_clientSalt.toLatin1())
                                   .append(m_passwordHash)
                                   .append(QByteArray::number(cProtocolVersion))
                                   .append("!hedgewars"),
                               QCryptographicHash::Sha1)
          .toHex();

  send("PASSWORD", QStringList{hash, m_clientSalt});
}

void NetSession::setSessionState(NetSession::SessionState sessionState) {
  if (m_sessionState == sessionState) return;

  m_sessionState = sessionState;

  emit sessionStateChanged(sessionState);
}