diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 58947ea3..87425cf1 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -21,13 +21,13 @@ set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "Autogen") find_package(Qt6 REQUIRED COMPONENTS Widgets Core Gui Network Xml RemoteObjects Quick Svg QuickControls2 - Core5Compat + Core5Compat Concurrent ) set(LIBS ${LIBS} Qt6::Widgets Qt6::Core Qt6::Gui Qt6::Network Qt6::Xml Qt6::RemoteObjects Qt6::Quick Qt6::Svg Qt6::QuickControls2 - Qt6::Core5Compat + Qt6::Core5Compat Qt6::Concurrent ) qt_standard_project_setup() @@ -71,6 +71,8 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/ui/uilogic.h ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.h + ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.h + ${CMAKE_CURRENT_LIST_DIR}/core/sshsession.h ) if(NOT IOS) @@ -93,6 +95,8 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/ui/uilogic.cpp ${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp ${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp + ${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp + ${CMAKE_CURRENT_LIST_DIR}/core/sshsession.cpp ) if(NOT IOS) diff --git a/client/core/servercontroller.cpp b/client/core/servercontroller.cpp index 40e6eda7..ae9ae74a 100644 --- a/client/core/servercontroller.cpp +++ b/client/core/servercontroller.cpp @@ -43,12 +43,10 @@ using namespace QSsh; ServerController::ServerController(std::shared_ptr settings, QObject *parent) : m_settings(settings) { - ssh_init(); } ServerController::~ServerController() { - ssh_finalize(); } ErrorCode ServerController::connectToHost(const ServerCredentials &credentials, ssh_session &session) { @@ -58,14 +56,14 @@ ErrorCode ServerController::connectToHost(const ServerCredentials &credentials, } int port = credentials.port; - int log_verbosity = SSH_LOG_NOLOG; - std::string host_ip = credentials.hostName.toStdString(); - std::string host_username = credentials.userName.toStdString() + "@" + host_ip; + int logVerbosity = SSH_LOG_NOLOG; + std::string hostIp = credentials.hostName.toStdString(); + std::string hostUsername = credentials.userName.toStdString() + "@" + hostIp; - ssh_options_set(session, SSH_OPTIONS_HOST, host_ip.c_str()); + ssh_options_set(session, SSH_OPTIONS_HOST, hostIp.c_str()); ssh_options_set(session, SSH_OPTIONS_PORT, &port); - ssh_options_set(session, SSH_OPTIONS_USER, host_username.c_str()); - ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &log_verbosity); + ssh_options_set(session, SSH_OPTIONS_USER, hostUsername.c_str()); + ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &logVerbosity); int connection_result = ssh_connect(session); @@ -82,7 +80,7 @@ ErrorCode ServerController::connectToHost(const ServerCredentials &credentials, auth_result = ssh_userauth_publickey(session, auth_username.c_str(), priv_key); } else { - auth_result = ssh_userauth_password(session, auth_username.c_str(), credentials.password.toStdString().c_str()); + auth_result = ssh_userauth_password(session, auth_username.c_str(), credentials.password.toStdString().c_str()); } if (auth_result != SSH_OK) { @@ -98,67 +96,38 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr const std::function)> &cbReadStdOut, const std::function)> &cbReadStdErr) { - ssh_session ssh = ssh_new(); - - ErrorCode e = connectToHost(credentials, ssh); - if (e) { - ssh_free(ssh); - return e; + std::shared_ptr session = m_sshClient.getSession(); + if (!session) { + return ErrorCode::SshInternalError; } - ssh_channel channel = ssh_channel_new(ssh); - - if (channel == NULL) { - ssh_disconnect(ssh); - ssh_free(ssh); - return ErrorCode::SshAuthenticationError; - } - - ssh_channel_open_session(channel); - - if (ssh_channel_is_open(channel)) { - qDebug() << "SSH chanel opened"; - } else { - ssh_channel_free(channel); - ssh_disconnect(ssh); - ssh_free(ssh); - return ErrorCode::SshAuthenticationError; + auto error = session->initChannel(credentials); + if (error != ErrorCode::NoError) { + return error; } script.replace("\r", ""); qDebug() << "Run script " << script; - int exec_result = ssh_channel_request_pty(channel); - - ssh_channel_change_pty_size(channel, 80, 1024); - ssh_channel_request_shell(channel); - QString totalLine; const QStringList &lines = script.split("\n", Qt::SkipEmptyParts); for (int i = 0; i < lines.count(); i++) { QString currentLine = lines.at(i); - QString nextLine; - if (i + 1 < lines.count()) nextLine = lines.at(i+1); if (totalLine.isEmpty()) { totalLine = currentLine; - } - else { + } else { totalLine = totalLine + "\n" + currentLine; } QString lineToExec; if (currentLine.endsWith("\\")) { continue; - } - else { + } else { lineToExec = totalLine; totalLine.clear(); } - qDebug()<<"total line " << totalLine; - - // Run collected line if (lineToExec.startsWith("#")) { continue; } @@ -167,43 +136,14 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr qDebug().noquote() << "EXEC" << lineToExec; Debug::appendSshLog("Run command:" + lineToExec); - int nwritten, nbytes = 0, tryes = 0; - char buffer[2048]; - if (ssh_channel_write(channel, lineToExec.toUtf8(), (uint32_t)lineToExec.size()) == lineToExec.size() && - ssh_channel_write(channel, "\n", 1) == 1){ - while (nbytes !=0 || tryes < 100){ - if (ssh_channel_is_open(channel) && !ssh_channel_is_eof(channel)) { - //nbytes = ssh_channel_read_nonblocking(channel, buffer, sizeof(buffer), 0); - nbytes = ssh_channel_read_timeout(channel, buffer, sizeof(buffer), 0, 50); - - if (nbytes > 0) { - tryes = 0; - std::string strbuf; - strbuf.assign(buffer, nbytes); - QByteArray qbuff(buffer, nbytes); - QString outp(qbuff); - - if (cbReadStdOut){ - cbReadStdOut(outp, nullptr); - } - qDebug().noquote() << QString(strbuf.c_str()); - - } else { - tryes++; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - } + error = session->writeToChannel(lineToExec); + if (error != ErrorCode::NoError) { + return error; } } - ssh_channel_send_eof(channel); - ssh_channel_free(channel); - ssh_disconnect(ssh); - ssh_free(ssh); - + qDebug() << "ServerController::runScript finished\n"; return ErrorCode::NoError; } @@ -214,7 +154,7 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti const std::function)> &cbReadStdErr) { QString fileName = "/opt/amnezia/" + Utils::getRandomString(16) + ".sh"; - Debug::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + ":\n" + script); + Debug::appendSshLog("Run container script for " + ContainerProps::containerToString(container) + QStringLiteral(":\n") + script); ErrorCode e = uploadTextFileToContainer(container, credentials, script, fileName); if (e) return e; @@ -888,60 +828,6 @@ QString ServerController::checkSshConnection(const ServerCredentials &credential return stdOut; } -SshConnection *ServerController::connectToHost(const SshConnectionParameters &sshParams) -{ - SshConnection *client = acquireConnection(sshParams); - if (!client) return nullptr; - - QEventLoop waitssh; - QObject::connect(client, &SshConnection::connected, &waitssh, [&]() { - qDebug() << "Server connected by ssh"; - waitssh.quit(); - }); - - QObject::connect(client, &SshConnection::disconnected, &waitssh, [&]() { - qDebug() << "Server disconnected by ssh"; - waitssh.quit(); - }); - - QObject::connect(client, &SshConnection::error, &waitssh, [&](QSsh::SshError error) { - qCritical() << "Ssh error:" << error << client->errorString(); - waitssh.quit(); - }); - - - // QObject::connect(client, &SshConnection::dataAvailable, [&](const QString &message) { - // qCritical() << "Ssh message:" << message; - // }); - - //qDebug() << "Connection state" << client->state(); - - if (client->state() == SshConnection::State::Unconnected) { - client->connectToHost(); - waitssh.exec(); - } - - - // QObject::connect(&client, &SshClient::sshDataReceived, [&](){ - // qDebug().noquote() << "Data received"; - // }); - - - // if(client.sshState() != SshClient::SshState::Ready) { - // qCritical() << "Can't connect to server"; - // return false; - // } - // else { - // qDebug() << "SSh connection established"; - // } - - - // QObject::connect(proc, &SshProcess::finished, &wait, &QEventLoop::quit); - // QObject::connect(proc, &SshProcess::failed, &wait, &QEventLoop::quit); - - return client; -} - void ServerController::disconnectFromHost(const ServerCredentials &credentials) { SshConnection *client = acquireConnection(sshParams(credentials)); diff --git a/client/core/servercontroller.h b/client/core/servercontroller.h index 60d9270b..43d5789f 100644 --- a/client/core/servercontroller.h +++ b/client/core/servercontroller.h @@ -15,6 +15,7 @@ #include "containers/containers_defs.h" #include "sftpdefs.h" +#include "sshclient.h" class Settings; class VpnConfigurator; @@ -76,7 +77,6 @@ public: Vars genVarsForScript(const ServerCredentials &credentials, DockerContainer container = DockerContainer::None, const QJsonObject &config = QJsonObject()); QString checkSshConnection(const ServerCredentials &credentials, ErrorCode *errorCode = nullptr); - QSsh::SshConnection *connectToHost(const QSsh::SshConnectionParameters &sshParams); private: @@ -92,6 +92,8 @@ private: std::shared_ptr m_settings; std::shared_ptr m_configurator; + + SshClient m_sshClient; }; #endif // SERVERCONTROLLER_H diff --git a/client/core/sshclient.cpp b/client/core/sshclient.cpp new file mode 100644 index 00000000..11ef2348 --- /dev/null +++ b/client/core/sshclient.cpp @@ -0,0 +1,19 @@ +#include "sshclient.h" + +#include +#include + +SshClient::SshClient(QObject *parent) : QObject(parent) +{ + ssh_init(); +} + +SshClient::~SshClient() +{ + ssh_finalize(); +} + +std::shared_ptr SshClient::getSession() +{ + return std::make_shared(); +} diff --git a/client/core/sshclient.h b/client/core/sshclient.h new file mode 100644 index 00000000..c58a776a --- /dev/null +++ b/client/core/sshclient.h @@ -0,0 +1,20 @@ +#ifndef SSHCLIENT_H +#define SSHCLIENT_H + +#include + +#include "sshsession.h" + +using namespace amnezia; + +class SshClient : public QObject +{ + Q_OBJECT +public: + SshClient(QObject *parent = nullptr); + ~SshClient(); + + std::shared_ptr getSession(); +}; + +#endif // SSHCLIENT_H diff --git a/client/core/sshsession.cpp b/client/core/sshsession.cpp new file mode 100644 index 00000000..9c779cdf --- /dev/null +++ b/client/core/sshsession.cpp @@ -0,0 +1,167 @@ +#include "sshsession.h" + +#include +#include + +SshSession::SshSession(QObject *parent) : QObject(parent) +{ + +} + +SshSession::~SshSession() +{ + if (m_isNeedSendChannelEof) { + ssh_channel_send_eof(m_channel); + } + if (m_isChannelOpened) { + ssh_channel_free(m_channel); + } + if (m_isSessionConnected) { + ssh_disconnect(m_session); + } + ssh_free(m_session); +} + +ErrorCode SshSession::connectToHost(const ServerCredentials &credentials) +{ + if (m_session == NULL) { + return ErrorCode::InternalError; + } + + int port = credentials.port; + int logVerbosity = SSH_LOG_NOLOG; + std::string hostIp = credentials.hostName.toStdString(); + std::string hostUsername = credentials.userName.toStdString() + "@" + hostIp; + + ssh_options_set(m_session, SSH_OPTIONS_HOST, hostIp.c_str()); + ssh_options_set(m_session, SSH_OPTIONS_PORT, &port); + ssh_options_set(m_session, SSH_OPTIONS_USER, hostUsername.c_str()); + ssh_options_set(m_session, SSH_OPTIONS_LOG_VERBOSITY, &logVerbosity); + + int connectionResult = ssh_connect(m_session); + + if (connectionResult != SSH_OK) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshTimeoutError; + } + + m_isSessionConnected = true; + + std::string authUsername = credentials.userName.toStdString(); + + int authResult = SSH_ERROR; + if (credentials.password.contains("BEGIN") && credentials.password.contains("PRIVATE KEY")) { + ssh_key privateKey; + ssh_pki_import_privkey_base64(credentials.password.toStdString().c_str(), nullptr, nullptr, nullptr, &privateKey); + authResult = ssh_userauth_publickey(m_session, authUsername.c_str(), privateKey); + } + else { + authResult = ssh_userauth_password(m_session, authUsername.c_str(), credentials.password.toStdString().c_str()); + } + + if (authResult != SSH_OK) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshAuthenticationError; + } + + return ErrorCode::NoError; +} + +ErrorCode SshSession::initChannel(const ServerCredentials &credentials) +{ + m_session = ssh_new(); + + ErrorCode error = connectToHost(credentials); + if (error) { + return error; + } + m_channel = ssh_channel_new(m_session); + + if (m_channel == NULL) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshAuthenticationError; + } + + int result = ssh_channel_open_session(m_channel); + + if (result == SSH_OK && ssh_channel_is_open(m_channel)) { + qDebug() << "SSH chanel opened"; + } else { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshAuthenticationError; + } + + result = ssh_channel_request_pty(m_channel); + if (result != SSH_OK) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshInternalError; + } + + result = ssh_channel_change_pty_size(m_channel, 80, 1024); + if (result != SSH_OK) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshInternalError; + } + + result = ssh_channel_request_shell(m_channel); + if (result != SSH_OK) { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshInternalError; + } + + return ErrorCode::NoError; +} + +ErrorCode SshSession::writeToChannel(const QString &data) +{ + QFutureWatcher watcher; + connect(&watcher, &QFutureWatcher::finished, this, &SshSession::writeToChannelFinished); + + QFuture future = QtConcurrent::run([this, &data]() { + return write(data); + }); + watcher.setFuture(future); + + QEventLoop wait; + + QObject::connect(this, &SshSession::writeToChannelFinished, &wait, &QEventLoop::quit); + wait.exec(); + + return watcher.result(); +} + +ErrorCode SshSession::write(const QString &data) +{ + const int channelReadTimeoutMs = 10; + const size_t bufferSize = 2048; + + int bytesToRead = 0; + int attempts = 0; + char buffer[bufferSize]; + + int bytesWritten = ssh_channel_write(m_channel, data.toUtf8(), (uint32_t)data.size()); + if (bytesWritten == data.size()) { + while (bytesToRead != 0 || attempts < 100){ + if (ssh_channel_is_open(m_channel) && !ssh_channel_is_eof(m_channel)) { + bytesToRead = ssh_channel_read_timeout(m_channel, buffer, sizeof(buffer), 0, channelReadTimeoutMs); + if (bytesToRead > 0) { + attempts = 0; + std::string strbuf(buffer, bytesToRead); +// QByteArray qbuff(buffer, bytesToRead); +// QString outp(buffer); + +// if (cbReadStdOut){ +// cbReadStdOut(outp, nullptr); +// } + qDebug().noquote() << QString(strbuf.c_str()); + } else { + attempts++; + } + } + } + } else { + qDebug() << ssh_get_error(m_session); + return ErrorCode::SshInternalError; + } + return ErrorCode::NoError; +} diff --git a/client/core/sshsession.h b/client/core/sshsession.h new file mode 100644 index 00000000..419b90ef --- /dev/null +++ b/client/core/sshsession.h @@ -0,0 +1,36 @@ +#ifndef SSHSESSION_H +#define SSHSESSION_H + +#include + +#include +#include + +#include "defs.h" + +using namespace amnezia; + +class SshSession : public QObject +{ + Q_OBJECT +public: + SshSession(QObject *parent = nullptr); + ~SshSession(); + + ErrorCode initChannel(const ServerCredentials &credentials); + ErrorCode writeToChannel(const QString &data); +private: + ErrorCode connectToHost(const ServerCredentials &credentials); + ErrorCode write(const QString &data); + + ssh_session m_session; + ssh_channel m_channel; + + bool m_isChannelOpened = false; + bool m_isSessionConnected = false; + bool m_isNeedSendChannelEof = false; +signals: + void writeToChannelFinished(); +}; + +#endif // SSHSESSION_H