Commit a48c6b6b authored by Albert Astals Cid's avatar Albert Astals Cid Committed by Albert Astals Cid

Add option of connecting to the vnc server through a ssh tunnel

Summary: Introduces an optional dependency on libssh

Test Plan: Connect to VNC servers both via SSH tunnel and via regular connection

Reviewers: dfaure, uwolfer

Reviewed By: dfaure, uwolfer

Subscribers: dfaure, acelankao, uwolfer, #kde_applications

Differential Revision: https://phabricator.kde.org/D13659
parent 98e814da
......@@ -74,6 +74,13 @@ set_package_properties("libvncserver" PROPERTIES
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)
......
# - 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 <asn@cryptomilk.org>
#
# 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)
......@@ -201,6 +201,16 @@ QUrl RemoteView::url()
#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 +229,6 @@ QString RemoteView::readWalletPassword(bool fromUserNameOnly)
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 +240,8 @@ QString RemoteView::readWalletPassword(bool fromUserNameOnly)
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);
......
......@@ -414,6 +414,8 @@ protected:
#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
......
......@@ -13,6 +13,12 @@ if(LIBVNCSERVER_FOUND)
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 @@ if(LIBVNCSERVER_FOUND)
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
)
......
......@@ -544,11 +544,6 @@ bool VncClientThread::clientCreate(bool reinitialising)
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 ---------------------";
......
......@@ -27,6 +27,12 @@
#include <QDesktopWidget>
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 @@ QWidget* VncHostPreferences::createProtocolSpecificConfigPage()
updateScaling(windowedScale());
#ifdef LIBSSH_FOUND
connect(vncUi.use_ssh_tunnel, &QCheckBox::toggled, vncUi.ssh_groupBox, &QWidget::setVisible);
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 @@ void VncHostPreferences::acceptConfig()
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);
}
......@@ -38,12 +38,22 @@ public:
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);
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0" >
<ui version="4.0">
<class>VncPreferences</class>
<widget class="QWidget" name="VncPreferences" >
<property name="geometry" >
<widget class="QWidget" name="VncPreferences">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>436</width>
<height>151</height>
<width>440</width>
<height>396</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" >
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="vncGroupBox" >
<property name="title" >
<widget class="QGroupBox" name="vncGroupBox">
<property name="title">
<string>Connection</string>
</property>
<layout class="QFormLayout" name="formLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="connectionLabel" >
<property name="text" >
<string>Connection type:</string>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="connectionLabel">
<property name="text">
<string>Connection &amp;type:</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>kcfg_Quality</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="KComboBox" name="kcfg_Quality" >
<property name="minimumSize" >
<item row="0" column="1">
<widget class="KComboBox" name="kcfg_Quality">
<property name="minimumSize">
<size>
<width>280</width>
<height>0</height>
</size>
</property>
<property name="whatsThis" >
<property name="whatsThis">
<string>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.</string>
</property>
<item>
<property name="text" >
<property name="text">
<string>High Quality (LAN, direct connection)</string>
</property>
</item>
<item>
<property name="text" >
<property name="text">
<string>Medium Quality (DSL, Cable, fast Internet)</string>
</property>
</item>
<item>
<property name="text" >
<property name="text">
<string>Low Quality (Modem, ISDN, slow Internet)</string>
</property>
</item>
......@@ -199,11 +199,64 @@
</widget>
</item>
<item>
<spacer name="verticalSpacer" >
<property name="orientation" >
<widget class="QCheckBox" name="use_ssh_tunnel">
<property name="text">
<string>Connect via SSH tunnel</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="ssh_groupBox">
<property name="title">
<string>SSH tunnel options</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="use_loopback">
<property name="text">
<string>Tunnel via loopback address</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Port:</string>
</property>
<property name="buddy">
<cstring>ssh_tunnel_port</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="ssh_tunnel_port">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>User name:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="ssh_tunnel_user_name"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<property name="sizeHint" stdset="0">
<size>
<width>428</width>
<height>16</height>
......
/****************************************************************************
**
** 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 <KLocalizedString>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <QDebug>
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)
{
}
VncSshTunnelThread::~VncSshTunnelThread()
{
m_stop_thread = true;
wait();
}
QString VncSshTunnelThread::password() const
{
return m_password;
}
// This is called by the main thread, but from a slot connected to our signal via BlockingQueuedConnection
// so this is safe even without a mutex, the semaphore in BlockingQueuedConnection takes care of the synchronization.
void VncSshTunnelThread::setPassword(const QString &password, PasswordOrigin origin)
{
m_password = password;
m_passwordOrigin = origin;
}
void VncSshTunnelThread::run()
{
struct CleanupHelper
{
int server_sock = -1;
int client_sock = -1;
ssh_session session = nullptr;
ssh_channel forwarding_channel = nullptr;
~CleanupHelper()
{
// the ssh functions just return if the param is null
ssh_channel_free(forwarding_channel);
if (client_sock != -1) {
close(client_sock);
}
if (server_sock != -1) {
close(server_sock);
}
ssh_disconnect(session);
ssh_free(session);
}
};
CleanupHelper cleanup;
ssh_session session = ssh_new();
if (session == nullptr)
return;
cleanup.session = session;
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))));
return;
}
const int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
emit errorMessage(i18n("Error creating tunnel socket"));
return;
}
cleanup.server_sock = server_sock;
// so that we can bind more than once in case more than one tunnel is used
int sockopt = 1;
setsockopt(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(server_sock, (struct sockaddr*)&sin, sizeof sin) == -1) {
emit errorMessage(i18n("Error creating tunnel socket"));
return;
}
}
if (listen(server_sock, 1) == -1) {
emit errorMessage(i18n("Error creating tunnel socket"));
return;
}
if (m_stop_thread) {
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 socket error here will be detected by 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(server_sock, (struct sockaddr *)&client_sin, &client_sin_len);
if (client_sock == -1)
{
qCDebug(KRDC) << "Error on tunnel socket accept";
return;
}
cleanup.client_sock = client_sock;
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);