diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,13 @@ PURPOSE "Needed to build VNC client support in KRDC" ) +find_package(LibSSH) +set_package_properties("libssh" PROPERTIES + DESCRIPTION "ssh library" + URL "http://libssh.org" + PURPOSE "Needed to build VNC+SSH tunnel support " +) + # Needs porting FIND_PROGRAM(FREERDP_EXECUTABLE xfreerdp) if(FREERDP_EXECUTABLE) diff --git a/cmake/modules/FindLibSSH.cmake b/cmake/modules/FindLibSSH.cmake new file mode 100644 --- /dev/null +++ b/cmake/modules/FindLibSSH.cmake @@ -0,0 +1,79 @@ +# - Try to find LibSSH +# Once done this will define +# +# LIBSSH_FOUND - system has LibSSH +# LIBSSH_INCLUDE_DIR - the LibSSH include directory +# LIBSSH_LIBRARIES - Link these to use LibSSH +# +# Copyright (c) 2009-2014 Andreas Schneider +# +# Redistribution and use is allowed according to the terms of the New +# BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# + +find_path(LIBSSH_INCLUDE_DIR + NAMES + libssh/libssh.h + PATHS + /usr/include + /usr/local/include + /opt/local/include + /sw/include + ${CMAKE_INCLUDE_PATH} + ${CMAKE_INSTALL_PREFIX}/include +) + +find_library(SSH_LIBRARY + NAMES + ssh + libssh + PATHS + /usr/lib + /usr/local/lib + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib +) + +set(LIBSSH_LIBRARIES + ${LIBSSH_LIBRARIES} + ${SSH_LIBRARY} +) + +if (LIBSSH_INCLUDE_DIR AND LibSSH_FIND_VERSION) + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_MAJOR + REGEX "#define[ ]+LIBSSH_VERSION_MAJOR[ ]+[0-9]+") + + # Older versions of libssh like libssh-0.2 have LIBSSH_VERSION but not LIBSSH_VERSION_MAJOR + if (LIBSSH_VERSION_MAJOR) + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_MAJOR ${LIBSSH_VERSION_MAJOR}) + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_MINOR + REGEX "#define[ ]+LIBSSH_VERSION_MINOR[ ]+[0-9]+") + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_MINOR ${LIBSSH_VERSION_MINOR}) + file(STRINGS ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h LIBSSH_VERSION_PATCH + REGEX "#define[ ]+LIBSSH_VERSION_MICRO[ ]+[0-9]+") + string(REGEX MATCH "[0-9]+" LIBSSH_VERSION_PATCH ${LIBSSH_VERSION_PATCH}) + + set(LIBSSH_VERSION ${LIBSSH_VERSION_MAJOR}.${LIBSSH_VERSION_MINOR}.${LIBSSH_VERSION_PATCH}) + + else (LIBSSH_VERSION_MAJOR) + message(STATUS "LIBSSH_VERSION_MAJOR not found in ${LIBSSH_INCLUDE_DIR}/libssh/libssh.h, assuming libssh is too old") + set(LIBSSH_FOUND FALSE) + endif (LIBSSH_VERSION_MAJOR) +endif (LIBSSH_INCLUDE_DIR AND LibSSH_FIND_VERSION) + +# If the version is too old, but libs and includes are set, +# find_package_handle_standard_args will set LIBSSH_FOUND to TRUE again, +# so we need this if() here. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibSSH + REQUIRED_VARS + LIBSSH_LIBRARIES + LIBSSH_INCLUDE_DIR + VERSION_VAR + LIBSSH_VERSION) + +# show the LIBSSH_INCLUDE_DIRS and LIBSSH_LIBRARIES variables only in the advanced view +mark_as_advanced(LIBSSH_INCLUDE_DIR LIBSSH_LIBRARIES) diff --git a/core/remoteview.h b/core/remoteview.h --- a/core/remoteview.h +++ b/core/remoteview.h @@ -414,6 +414,8 @@ #ifndef QTONLY QString readWalletPassword(bool fromUserNameOnly = false); void saveWalletPassword(const QString &password, bool fromUserNameOnly = false); + QString readWalletPasswordForKey(const QString &key); + void saveWalletPasswordForKey(const QString &key, const QString &password); KWallet::Wallet *m_wallet; #endif diff --git a/core/remoteview.cpp b/core/remoteview.cpp --- a/core/remoteview.cpp +++ b/core/remoteview.cpp @@ -201,6 +201,17 @@ #ifndef QTONLY QString RemoteView::readWalletPassword(bool fromUserNameOnly) +{ + return readWalletPasswordForKey(fromUserNameOnly ? m_url.userName() : m_url.toDisplayString(QUrl::StripTrailingSlash)); +} + +void RemoteView::saveWalletPassword(const QString &password, bool fromUserNameOnly) +{ + saveWalletPasswordForKey(fromUserNameOnly ? m_url.userName() : m_url.toDisplayString(QUrl::StripTrailingSlash), password); + +} + +QString RemoteView::readWalletPasswordForKey(const QString &key) { const QString KRDCFOLDER = QLatin1String("KRDC"); @@ -219,12 +230,6 @@ m_wallet->setFolder(KRDCFOLDER); QString password; - QString key; - if (fromUserNameOnly) - key = m_url.userName(); - else - key = m_url.toDisplayString(QUrl::StripTrailingSlash); - if (m_wallet->hasEntry(key) && !m_wallet->readPassword(key, password)) { qCDebug(KRDC) << "Password read OK"; @@ -236,14 +241,8 @@ return QString(); } -void RemoteView::saveWalletPassword(const QString &password, bool fromUserNameOnly) +void RemoteView::saveWalletPasswordForKey(const QString &key, const QString &password) { - QString key; - if (fromUserNameOnly) - key = m_url.userName(); - else - key = m_url.toDisplayString(QUrl::StripTrailingSlash); - if (m_wallet && m_wallet->isOpen()) { qCDebug(KRDC) << "Write wallet password"; m_wallet->writePassword(key, password); diff --git a/vnc/CMakeLists.txt b/vnc/CMakeLists.txt --- a/vnc/CMakeLists.txt +++ b/vnc/CMakeLists.txt @@ -13,6 +13,12 @@ vncview.cpp ) + if (LIBSSH_FOUND) + include_directories(${LIBSSH_INCLUDE_DIR}) + add_definitions(-DLIBSSH_FOUND) + set(vncplugin_SRCS ${vncplugin_SRCS} vncsshtunnelthread.cpp) + endif() + ecm_qt_declare_logging_category(vncplugin_SRCS HEADER krdc_debug.h IDENTIFIER KRDC CATEGORY_NAME KRDC) ki18n_wrap_ui(vncplugin_SRCS @@ -34,6 +40,11 @@ target_link_libraries(krdc_vncplugin ${JPEG_LIBRARIES}) endif(JPEG_FOUND) + if (LIBSSH_FOUND) + target_link_libraries(krdc_vncplugin ${LIBSSH_LIBRARIES}) + endif() + + set(kcm_krdc_vncplugin_SRCS vncpreferences.cpp ) diff --git a/vnc/vncclientthread.cpp b/vnc/vncclientthread.cpp --- a/vnc/vncclientthread.cpp +++ b/vnc/vncclientthread.cpp @@ -544,11 +544,6 @@ cl->serverHost = strdup(m_host.toUtf8().constData()); - if (m_port < 0 || !m_port) // port is invalid or empty... - m_port = 5900; // fallback: try an often used VNC port - - if (m_port >= 0 && m_port < 100) // the user most likely used the short form (e.g. :1) - m_port += 5900; cl->serverPort = m_port; qCDebug(KRDC) << "--------------------- trying init ---------------------"; diff --git a/vnc/vnchostpreferences.h b/vnc/vnchostpreferences.h --- a/vnc/vnchostpreferences.h +++ b/vnc/vnchostpreferences.h @@ -38,12 +38,22 @@ void setQuality(RemoteView::Quality quality); RemoteView::Quality quality(); + bool useSshTunnel() const; + bool useSshTunnelLoopback() const; + int sshTunnelPort() const; + QString sshTunnelUserName() const; + protected: void acceptConfig() override; QWidget* createProtocolSpecificConfigPage() override; private: + void setUseSshTunnel(bool useSshTunnel); + void setUseSshTunnelLoopback(bool useSshTunnelLoopback); + void setSshTunnelPort(int port); + void setSshTunnelUserName(const QString &userName); + Ui::VncPreferences vncUi; void checkEnableCustomSize(int index); diff --git a/vnc/vnchostpreferences.cpp b/vnc/vnchostpreferences.cpp --- a/vnc/vnchostpreferences.cpp +++ b/vnc/vnchostpreferences.cpp @@ -27,6 +27,12 @@ #include +static const char *quality_config_key = "quality"; +static const char *use_ssh_tunnel_config_key = "use_ssh_tunnel"; +static const char *use_ssh_tunnel_loopback_config_key = "use_ssh_tunnel_loopback"; +static const char *ssh_tunnel_port_config_key = "ssh_tunnel_port"; +static const char *ssh_tunnel_user_name_config_key = "ssh_tunnel_user_name"; + VncHostPreferences::VncHostPreferences(KConfigGroup configGroup, QObject *parent) : HostPreferences(configGroup, parent) { @@ -55,6 +61,19 @@ updateScaling(windowedScale()); + connect(vncUi.use_ssh_tunnel, &QCheckBox::toggled, vncUi.ssh_groupBox, &QWidget::setVisible); + +#ifdef LIBSSH_FOUND + vncUi.ssh_groupBox->setVisible(useSshTunnel()); + vncUi.use_ssh_tunnel->setChecked(useSshTunnel()); + vncUi.use_loopback->setChecked(useSshTunnelLoopback()); + vncUi.ssh_tunnel_port->setValue(sshTunnelPort()); + vncUi.ssh_tunnel_user_name->setText(sshTunnelUserName()); +#else + vncUi.ssh_groupBox->hide(); + vncUi.use_ssh_tunnel->hide(); +#endif + return vncPage; } @@ -125,16 +144,60 @@ setHeight(vncUi.kcfg_ScalingHeight->value()); setWidth(vncUi.kcfg_ScalingWidth->value()); } + + setUseSshTunnel(vncUi.use_ssh_tunnel->isChecked()); + setUseSshTunnelLoopback(vncUi.use_loopback->isChecked()); + setSshTunnelPort(vncUi.ssh_tunnel_port->value()); + setSshTunnelUserName(vncUi.ssh_tunnel_user_name->text()); } void VncHostPreferences::setQuality(RemoteView::Quality quality) { if (quality >= 0 && quality <= 3) - m_configGroup.writeEntry("quality", (int) quality); + m_configGroup.writeEntry(quality_config_key, (int) quality); } RemoteView::Quality VncHostPreferences::quality() { - return (RemoteView::Quality) m_configGroup.readEntry("quality", (int) Settings::quality() + 1); + return (RemoteView::Quality) m_configGroup.readEntry(quality_config_key, (int) Settings::quality() + 1); +} + +bool VncHostPreferences::useSshTunnel() const +{ + return m_configGroup.readEntry(use_ssh_tunnel_config_key, false); } +void VncHostPreferences::setUseSshTunnel(bool useSshTunnel) +{ + m_configGroup.writeEntry(use_ssh_tunnel_config_key, useSshTunnel); +} + +bool VncHostPreferences::useSshTunnelLoopback() const +{ + return m_configGroup.readEntry(use_ssh_tunnel_loopback_config_key, false); +} + +void VncHostPreferences::setUseSshTunnelLoopback(bool useSshTunnelLoopback) +{ + m_configGroup.writeEntry(use_ssh_tunnel_loopback_config_key, useSshTunnelLoopback); +} + +int VncHostPreferences::sshTunnelPort() const +{ + return m_configGroup.readEntry(ssh_tunnel_port_config_key, 22); +} + +void VncHostPreferences::setSshTunnelPort(int port) +{ + m_configGroup.writeEntry(ssh_tunnel_port_config_key, port); +} + +QString VncHostPreferences::sshTunnelUserName() const +{ + return m_configGroup.readEntry(ssh_tunnel_user_name_config_key, QString()); +} + +void VncHostPreferences::setSshTunnelUserName(const QString &userName) +{ + m_configGroup.writeEntry(ssh_tunnel_user_name_config_key, userName); +} diff --git a/vnc/vncpreferences.ui b/vnc/vncpreferences.ui --- a/vnc/vncpreferences.ui +++ b/vnc/vncpreferences.ui @@ -1,55 +1,55 @@ - + VncPreferences - - + + 0 0 - 436 - 151 + 440 + 396 - + - - + + Connection - - - - - Connection type: + + + + + Connection &type: - + kcfg_Quality - - - + + + 280 0 - + Use this to specify the performance of your connection. Note that you should select the speed of the weakest link - even if you have a high speed connection, it will not help you if the remote computer uses a slow modem. Choosing a level of quality that is too high on a slow link will cause slower response times. Choosing a lower quality will increase latencies in high speed connections and results in lower image quality, especially in 'Low Quality' mode. - + High Quality (LAN, direct connection) - + Medium Quality (DSL, Cable, fast Internet) - + Low Quality (Modem, ISDN, slow Internet) @@ -165,7 +165,7 @@ false - H&eight: + Hei&ght: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -199,11 +199,64 @@ - - + + + Connect via SSH tunnel + + + + + + + SSH tunnel options + + + + + + Tunnel via loopback address + + + + + + + Port: + + + ssh_tunnel_port + + + + + + + 1 + + + 65535 + + + + + + + User name: + + + + + + + + + + + + Qt::Vertical - + 428 16 diff --git a/vnc/vncsshtunnelthread.h b/vnc/vncsshtunnelthread.h new file mode 100644 --- /dev/null +++ b/vnc/vncsshtunnelthread.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group +** company, info@kdab.com. Work sponsored by the +** LiMux project of the city of Munich +** +** This file is part of KRDC. +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; see the file COPYING. If not, write to +** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +** Boston, MA 02110-1301, USA. +** +****************************************************************************/ + +#ifndef VCNCSSHTUNNELTHREAD_H +#define VCNCSSHTUNNELTHREAD_H + +#include + +#include +#include + +#include + +class VncSshTunnelThread : public QThread +{ + Q_OBJECT +public: + VncSshTunnelThread(const QByteArray &host, int vncPort, int tunnelPort, int sshPort, const QByteArray &sshUserName, bool loopback); + + enum PasswordOrigin { + PasswordFromWallet, + PasswordFromDialog + }; + + enum PasswordRequestFlags { + NoFlags, + IgnoreWallet + }; + + QString password() const; + void setPassword(const QString &password, PasswordOrigin origin); + + void run() override; + void stop(); + +Q_SIGNALS: + void passwordRequest(PasswordRequestFlags flags); + void listenReady(); + void errorMessage(const QString &message); + +private: + QByteArray m_host; + int m_vncPort; + int m_tunnelPort; + int m_sshPort; + QByteArray m_sshUserName; + bool m_loopback; + QString m_password; + PasswordOrigin m_passwordOrigin; + + std::atomic_bool m_stop_thread; + int m_server_sock; +}; + +#endif diff --git a/vnc/vncsshtunnelthread.cpp b/vnc/vncsshtunnelthread.cpp new file mode 100644 --- /dev/null +++ b/vnc/vncsshtunnelthread.cpp @@ -0,0 +1,286 @@ +/**************************************************************************** +** +** Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group +** company, info@kdab.com. Work sponsored by the +** LiMux project of the city of Munich +** +** This file is part of KRDC. +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MEresHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; see the file COPYING. If not, write to +** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +** Boston, MA 02110-1301, USA. +** +****************************************************************************/ + +#include "vncsshtunnelthread.h" +#include "krdc_debug.h" + +#include + +#include + +#include +#include +#include +#include + +#include + +VncSshTunnelThread::VncSshTunnelThread(const QByteArray &host, int vncPort, int tunnelPort, int sshPort, const QByteArray &sshUserName, bool loopback) : + m_host(host), + m_vncPort(vncPort), + m_tunnelPort(tunnelPort), + m_sshPort(sshPort), + m_sshUserName(sshUserName), + m_loopback(loopback), + m_stop_thread(false) +{ +} + +QString VncSshTunnelThread::password() const +{ + return m_password; +} + +void VncSshTunnelThread::setPassword(const QString &password, PasswordOrigin origin) +{ + m_password = password; + m_passwordOrigin = origin; +} + +void VncSshTunnelThread::run() +{ + ssh_session session = ssh_new(); + if (session == nullptr) + return; + + ssh_options_set(session, SSH_OPTIONS_HOST, m_host.constData()); + ssh_options_set(session, SSH_OPTIONS_USER, m_sshUserName.constData()); + ssh_options_set(session, SSH_OPTIONS_PORT, &m_sshPort); + + int res = ssh_connect(session); + if (res != SSH_OK) + { + emit errorMessage(i18n("Error connecting to %1: %2", QString::fromUtf8(m_host), QString::fromLocal8Bit(ssh_get_error(session)))); + return; + } + + // First try authenticating via ssh agent + res = ssh_userauth_agent(session, NULL); + + if (res != SSH_AUTH_SUCCESS) { + // If ssh agent didn't work, try with password + emit passwordRequest(NoFlags); // This calls blockingly to the main thread which will call setPassword + res = ssh_userauth_password(session, NULL, m_password.toUtf8().constData()); + + // If password didn't work but came from the wallet, ask the user for the password + if (res != SSH_AUTH_SUCCESS && m_passwordOrigin == PasswordFromWallet) { + emit passwordRequest(IgnoreWallet); // This calls blockingly to the main thread which will call setPassword + res = ssh_userauth_password(session, NULL, m_password.toUtf8().constData()); + } + } + + if (res != SSH_AUTH_SUCCESS) { + emit errorMessage(i18n("Error authenticating with password: %1", QString::fromLocal8Bit(ssh_get_error(session)))); + ssh_disconnect(session); + ssh_free(session); + return; + } + + m_server_sock = socket(AF_INET, SOCK_STREAM, 0); + if (m_server_sock == -1) { + emit errorMessage(i18n("Error creating tunnel socket")); + ssh_disconnect(session); + ssh_free(session); + return; + } + + // so that we can bind more than once in case more than one tunnel is used + int sockopt = 1; + setsockopt(m_server_sock , SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)); + + { + // bind the server socket + struct sockaddr_in sin; + sin.sin_family = AF_INET; + sin.sin_port = htons(m_tunnelPort); + sin.sin_addr.s_addr = inet_addr("127.0.0.1"); + + if(bind(m_server_sock, (struct sockaddr*)&sin, sizeof sin) == -1) { + emit errorMessage(i18n("Error creating tunnel socket")); + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); + return; + } + } + + if(listen(m_server_sock, 1) == -1) { + emit errorMessage(i18n("Error creating tunnel socket")); + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); + return; + } + + if (m_stop_thread) { + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); + return; + } + + emit listenReady(); + // After here we don't need to emit errorMessage anymore on error, qCDebug is enough + // this is because the actual vnc thread will start because of this call and thus + // any error here will propagate to the vnc thread and the usual error mechanisms + // there will warn the user interface + + int client_sock; + { + struct sockaddr_in client_sin; + socklen_t client_sin_len = sizeof client_sin; + client_sock = accept(m_server_sock, (struct sockaddr *)&client_sin, &client_sin_len); + if (client_sock == -1) + { + qCDebug(KRDC) << "Error on tunnel socket accept"; + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); + return; + } + + int sock_flags = fcntl(client_sock, F_GETFL, 0); + fcntl(client_sock, F_SETFL, sock_flags | O_NONBLOCK); + } + + ssh_channel forwarding_channel = ssh_channel_new(session); + { + const char *forward_remote_host = m_loopback ? "127.0.0.1" : m_host.constData(); + res = ssh_channel_open_forward(forwarding_channel, forward_remote_host, m_vncPort, "127.0.0.1", 0); + if (res != SSH_OK || !ssh_channel_is_open(forwarding_channel)) + { + qCDebug(KRDC) << "SSH channel open error" << ssh_get_error(session); + ssh_channel_free(forwarding_channel); + close(client_sock); + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); + return; + } + } + + char client_read_buffer[40960]; + char *channel_read_buffer = nullptr; + char *channel_read_buffer_ptr = nullptr; + int channel_read_buffer_to_write; + while(!m_stop_thread && !ssh_channel_is_eof (forwarding_channel)) { + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 200000; + + fd_set set; + FD_ZERO(&set); + FD_SET(client_sock, &set); + ssh_channel channels[2] = { forwarding_channel, NULL }; + ssh_channel channels_out[2] = { NULL, NULL }; + + res = ssh_select(channels, channels_out, client_sock + 1, &set, &timeout); + if (res == SSH_EINTR) continue; + if (res == -1) break; + + bool error = false; + if (FD_ISSET(client_sock, &set)) { + int bytes_read; + while (!error && (bytes_read = read(client_sock, client_read_buffer, sizeof client_read_buffer)) > 0) { + int bytes_written = 0; + int bytes_to_write = bytes_read; + for (char *ptr = client_read_buffer; + bytes_to_write > 0; + bytes_to_write -= bytes_written, ptr += bytes_written) + { + bytes_written = ssh_channel_write(forwarding_channel, ptr, bytes_to_write); + if (bytes_written <= 0) { + error = true; + qCDebug(KRDC) << "error on ssh_channel_write"; + break; + } + } + } + if (bytes_read == 0) { + qCDebug(KRDC) << "error on tunnel read"; + error = true; + } + } + + if (error) { + continue; + } + + // If on the previous iteration we successfully wrote all we read, we need to read again + if (!channel_read_buffer) { + const int bytes_available = ssh_channel_poll(forwarding_channel, 0); + if (bytes_available == SSH_ERROR || bytes_available == SSH_EOF) { + qCDebug(KRDC) << "error on ssh_channel_poll"; + error = true; + } else if (bytes_available > 0) { + channel_read_buffer = new char[bytes_available]; + channel_read_buffer_ptr = channel_read_buffer; + const int bytes_read = ssh_channel_read_nonblocking(forwarding_channel, channel_read_buffer, bytes_available, 0); + if (bytes_read <= 0) { + qCDebug(KRDC) << "error on ssh_channel_read_nonblocking"; + error = true; + } else { + channel_read_buffer_to_write = bytes_read; + } + } + } + + if (!error && channel_read_buffer) { + for (int bytes_written = 0; + channel_read_buffer_to_write > 0; + channel_read_buffer_to_write -= bytes_written, channel_read_buffer_ptr += bytes_written) + { + bytes_written = write(client_sock, channel_read_buffer_ptr, channel_read_buffer_to_write); + if (bytes_written == -1 && errno == EAGAIN) { + // socket is full, just carry on and we will write on the next iteration + // that is why the previous code does 'if (!channel_read_buffer)' + break; + } + if (bytes_written <= 0) { + qCDebug(KRDC) << "error on tunnel write"; + error = true; + break; + } + } + if (channel_read_buffer_to_write <= 0) { + delete[] channel_read_buffer; + channel_read_buffer = nullptr; + } + } + } + + ssh_channel_free(forwarding_channel); + close(client_sock); + close(m_server_sock); + ssh_disconnect(session); + ssh_free(session); +} + +void VncSshTunnelThread::stop() +{ + m_stop_thread = true; + shutdown(m_server_sock, SHUT_RDWR); + wait(); +} diff --git a/vnc/vncview.h b/vnc/vncview.h --- a/vnc/vncview.h +++ b/vnc/vncview.h @@ -33,6 +33,11 @@ #include "vnchostpreferences.h" #endif +#ifdef LIBSSH_FOUND + #include "vncsshtunnelthread.h" +#endif + + #include #include @@ -95,6 +100,12 @@ #endif QImage m_frame; bool m_forceLocalCursor; +#ifdef LIBSSH_FOUND + VncSshTunnelThread *m_sshTunnelThread; + + QString readWalletSshPassword(); + void saveWalletSshPassword(); +#endif void keyEventHandler(QKeyEvent *e); void unpressModifiers(); @@ -105,7 +116,11 @@ void updateImage(int x, int y, int w, int h); void setCut(const QString &text); void requestPassword(bool includingUsername); +#ifdef LIBSSH_FOUND + void sshRequestPassword(VncSshTunnelThread::PasswordRequestFlags flags); +#endif void outputErrorMessage(const QString &message); + void sshErrorMessage(const QString &message); void clipboardDataChanged(); }; diff --git a/vnc/vncview.cpp b/vnc/vncview.cpp --- a/vnc/vncview.cpp +++ b/vnc/vncview.cpp @@ -62,11 +62,20 @@ m_horizontalFactor(1.0), m_verticalFactor(1.0), m_forceLocalCursor(false) +#ifdef LIBSSH_FOUND + , m_sshTunnelThread(nullptr) +#endif { m_url = url; m_host = url.host(); m_port = url.port(); + if (m_port <= 0) // port is invalid or empty... + m_port = 5900; // fallback: try an often used VNC port + + if (m_port < 100) // the user most likely used the short form (e.g. :1) + m_port += 5900; + // BlockingQueuedConnection can cause deadlocks when exiting, handled in startQuitting() connect(&vncThread, SIGNAL(imageUpdated(int,int,int,int)), this, SLOT(updateImage(int,int,int,int)), Qt::BlockingQueuedConnection); connect(&vncThread, SIGNAL(gotCut(QString)), this, SLOT(setCut(QString)), Qt::BlockingQueuedConnection); @@ -168,6 +177,14 @@ vncThread.quit(); +#ifdef LIBSSH_FOUND + if (m_sshTunnelThread) { + m_sshTunnelThread->stop(); + delete m_sshTunnelThread; + m_sshTunnelThread = nullptr; + } +#endif + const bool quitSuccess = vncThread.wait(500); if (!quitSuccess) { // happens when vncThread wants to call a slot via BlockingQueuedConnection, @@ -191,8 +208,33 @@ bool VncView::start() { - vncThread.setHost(m_host); - vncThread.setPort(m_port); + QString vncHost = m_host; + int vncPort = m_port; +#ifdef LIBSSH_FOUND + if (m_hostPreferences->useSshTunnel()) { + Q_ASSERT(!m_sshTunnelThread); + + const int tunnelPort = 58219; // Just a random port + + m_sshTunnelThread = new VncSshTunnelThread(m_host.toUtf8(), + m_port, + tunnelPort, + m_hostPreferences->sshTunnelPort(), + m_hostPreferences->sshTunnelUserName().toUtf8(), + m_hostPreferences->useSshTunnelLoopback()); + connect(m_sshTunnelThread, &VncSshTunnelThread::passwordRequest, this, &VncView::sshRequestPassword, Qt::BlockingQueuedConnection); + connect(m_sshTunnelThread, &VncSshTunnelThread::errorMessage, this, &VncView::sshErrorMessage); + m_sshTunnelThread->start(); + + if (m_hostPreferences->useSshTunnelLoopback()) { + vncHost = QStringLiteral("127.0.0.1"); + } + vncPort = tunnelPort; + } +#endif + + vncThread.setHost(vncHost); + vncThread.setPort(vncPort); RemoteView::Quality quality; #ifdef QTONLY quality = (RemoteView::Quality)((QCoreApplication::arguments().count() > 2) ? @@ -216,7 +258,15 @@ setStatus(Connecting); - vncThread.start(); +#ifdef LIBSSH_FOUND + if (m_hostPreferences->useSshTunnel()) { + connect(m_sshTunnelThread, &VncSshTunnelThread::listenReady, this, [this] { vncThread.start(); }); + } + else +#endif + { + vncThread.start(); + } return true; } @@ -302,6 +352,31 @@ #endif } +#ifdef LIBSSH_FOUND +void VncView::sshRequestPassword(VncSshTunnelThread::PasswordRequestFlags flags) +{ + qCDebug(KRDC) << "request ssh password"; + + if (m_hostPreferences->walletSupport() && ((flags & VncSshTunnelThread::IgnoreWallet) != VncSshTunnelThread::IgnoreWallet)) { + const QString walletPassword = readWalletSshPassword(); + + if (!walletPassword.isNull()) { + m_sshTunnelThread->setPassword(walletPassword, VncSshTunnelThread::PasswordFromWallet); + return; + } + } + + KPasswordDialog dialog(this); + dialog.setPrompt(i18n("Please enter the SSH password.")); + if (dialog.exec() == KPasswordDialog::Accepted) { + m_sshTunnelThread->setPassword(dialog.password(), VncSshTunnelThread::PasswordFromDialog); + } else { + qCDebug(KRDC) << "ssh password dialog not accepted"; + startQuitting(); + } +} +#endif + void VncView::outputErrorMessage(const QString &message) { qCritical(KRDC) << message; @@ -319,6 +394,17 @@ emit errorMessage(i18n("VNC failure"), message); } +void VncView::sshErrorMessage(const QString &message) +{ + qCritical(KRDC) << message; + + startQuitting(); + + KMessageBox::error(this, message, i18n("SSH Tunnel failure")); + + emit errorMessage(i18n("SSH Tunnel failure"), message); +} + #ifndef QTONLY HostPreferences* VncView::hostPreferences() { @@ -376,6 +462,11 @@ #ifndef QTONLY if (m_hostPreferences->walletSupport()) { saveWalletPassword(vncThread.password()); +#ifdef LIBSSH_FOUND + if (m_hostPreferences->useSshTunnel()) { + saveWalletSshPassword(); + } +#endif } #endif } @@ -560,6 +651,18 @@ vncThread.mouseEvent(x, y, m_buttonMask); } +#ifdef LIBSSH_FOUND +QString VncView::readWalletSshPassword() +{ + return readWalletPasswordForKey(QStringLiteral("SSHTUNNEL") + m_url.toDisplayString(QUrl::StripTrailingSlash)); +} + +void VncView::saveWalletSshPassword() +{ + saveWalletPasswordForKey(QStringLiteral("SSHTUNNEL") + m_url.toDisplayString(QUrl::StripTrailingSlash), m_sshTunnelThread->password()); +} +#endif + void VncView::keyEventHandler(QKeyEvent *e) { // strip away autorepeating KeyRelease; see bug #206598