File indexing completed on 2024-05-05 09:12:58

0001 /*
0002     SPDX-FileCopyrightText: 2018 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003     Work sponsored by the LiMux project of the city of Munich
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "vncsshtunnelthread.h"
0009 #include "krdc_debug.h"
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <fcntl.h>
0014 #include <arpa/inet.h>
0015 #include <netinet/in.h>
0016 #include <sys/socket.h>
0017 
0018 #include <QDebug>
0019 
0020 VncSshTunnelThread::VncSshTunnelThread(const QByteArray &host, int vncPort, int tunnelPort, int sshPort, const QByteArray &sshUserName, bool loopback) :
0021     m_host(host),
0022     m_vncPort(vncPort),
0023     m_tunnelPort(tunnelPort),
0024     m_sshPort(sshPort),
0025     m_sshUserName(sshUserName),
0026     m_loopback(loopback),
0027     m_stop_thread(false)
0028 {
0029 }
0030 
0031 VncSshTunnelThread::~VncSshTunnelThread()
0032 {
0033     m_stop_thread = true;
0034     wait();
0035 }
0036 
0037 
0038 int VncSshTunnelThread::tunnelPort() const
0039 {
0040     return m_tunnelPort;
0041 }
0042 
0043 QString VncSshTunnelThread::password() const
0044 {
0045     return m_password;
0046 }
0047 
0048 // This is called by the main thread, but from a slot connected to our signal via BlockingQueuedConnection
0049 // so this is safe even without a mutex, the semaphore in BlockingQueuedConnection takes care of the synchronization.
0050 void VncSshTunnelThread::setPassword(const QString &password, PasswordOrigin origin)
0051 {
0052     m_password = password;
0053     m_passwordOrigin = origin;
0054 }
0055 
0056 // This is called by the main thread, but from a slot connected to our signal via BlockingQueuedConnection
0057 // so this is safe even without a mutex, the semaphore in BlockingQueuedConnection takes care of the synchronization.
0058 void VncSshTunnelThread::userCanceledPasswordRequest()
0059 {
0060     m_passwordRequestCanceledByUser = true;
0061 }
0062 
0063 void VncSshTunnelThread::run()
0064 {
0065     struct CleanupHelper
0066     {
0067         int server_sock = -1;
0068         int client_sock = -1;
0069         ssh_session session = nullptr;
0070         ssh_channel forwarding_channel = nullptr;
0071 
0072         ~CleanupHelper()
0073         {
0074             // the ssh functions just return if the param is null
0075             ssh_channel_free(forwarding_channel);
0076             if (client_sock != -1) {
0077                 close(client_sock);
0078             }
0079             if (server_sock != -1) {
0080                 close(server_sock);
0081             }
0082             ssh_disconnect(session);
0083             ssh_free(session);
0084         }
0085     };
0086 
0087     CleanupHelper cleanup;
0088 
0089     ssh_session session = ssh_new();
0090     if (session == nullptr)
0091         return;
0092 
0093     cleanup.session = session;
0094 
0095     ssh_options_set(session, SSH_OPTIONS_HOST, m_host.constData());
0096     ssh_options_set(session, SSH_OPTIONS_USER, m_sshUserName.constData());
0097     ssh_options_set(session, SSH_OPTIONS_PORT, &m_sshPort);
0098 
0099     int res = ssh_connect(session);
0100     if (res != SSH_OK)
0101     {
0102         Q_EMIT errorMessage(i18n("Error connecting to %1: %2", QString::fromUtf8(m_host), QString::fromLocal8Bit(ssh_get_error(session))));
0103         return;
0104     }
0105 
0106     // First try authenticating via ssh agent
0107     res = ssh_userauth_agent(session, nullptr);
0108 
0109     m_passwordRequestCanceledByUser = false;
0110     if (res != SSH_AUTH_SUCCESS) {
0111         // If ssh agent didn't work, try with password
0112         Q_EMIT passwordRequest(NoFlags); // This calls blockingly to the main thread which will call setPassword
0113         res = ssh_userauth_password(session, nullptr, m_password.toUtf8().constData());
0114 
0115         // If password didn't work but came from the wallet, ask the user for the password
0116         if (!m_passwordRequestCanceledByUser && res != SSH_AUTH_SUCCESS && m_passwordOrigin == PasswordFromWallet) {
0117             Q_EMIT passwordRequest(IgnoreWallet); // This calls blockingly to the main thread which will call setPassword
0118             res = ssh_userauth_password(session, nullptr, m_password.toUtf8().constData());
0119         }
0120     }
0121 
0122     if (m_passwordRequestCanceledByUser) {
0123         return;
0124     }
0125 
0126     if (res != SSH_AUTH_SUCCESS) {
0127         Q_EMIT errorMessage(i18n("Error authenticating with password: %1", QString::fromLocal8Bit(ssh_get_error(session))));
0128         return;
0129     }
0130 
0131     const int server_sock = socket(AF_INET, SOCK_STREAM, 0);
0132     if (server_sock == -1) {
0133         Q_EMIT errorMessage(i18n("Error creating tunnel socket"));
0134         return;
0135     }
0136 
0137     cleanup.server_sock = server_sock;
0138 
0139     // so that we can bind more than once in case more than one tunnel is used
0140     int sockopt = 1;
0141     setsockopt(server_sock , SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt));
0142 
0143     {
0144         // bind the server socket
0145         struct sockaddr_in sin;
0146         sin.sin_family = AF_INET;
0147         sin.sin_port = htons(m_tunnelPort);
0148         sin.sin_addr.s_addr = inet_addr("127.0.0.1");
0149 
0150         if (bind(server_sock, (struct sockaddr*)&sin, sizeof sin) == -1) {
0151             Q_EMIT errorMessage(i18n("Error creating tunnel socket"));
0152             return;
0153         }
0154 
0155         if (m_tunnelPort == 0) {
0156             socklen_t sin_len = sizeof sin;
0157             if (getsockname(server_sock, (struct sockaddr *)&sin, &sin_len) == -1) {
0158                 Q_EMIT errorMessage(i18n("Error creating tunnel socket"));
0159                 return;
0160             }
0161             m_tunnelPort = ntohs(sin.sin_port);
0162         }
0163     }
0164 
0165     if (listen(server_sock, 1) == -1) {
0166         Q_EMIT errorMessage(i18n("Error creating tunnel socket"));
0167         return;
0168     }
0169 
0170     if (m_stop_thread) {
0171         return;
0172     }
0173 
0174     Q_EMIT listenReady();
0175     // After here we don't need to emit errorMessage anymore on error, qCDebug is enough
0176     // this is because the actual vnc thread will start because of this call and thus
0177     // any socket error here will be detected by the vnc thread and the usual error mechanisms
0178     // there will warn the user interface
0179 
0180     int client_sock;
0181     {
0182         struct sockaddr_in client_sin;
0183         socklen_t client_sin_len = sizeof client_sin;
0184         client_sock = accept(server_sock, (struct sockaddr *)&client_sin, &client_sin_len);
0185         if (client_sock == -1)
0186         {
0187             qCDebug(KRDC) << "Error on tunnel socket accept";
0188             return;
0189         }
0190 
0191         cleanup.client_sock = client_sock;
0192 
0193         int sock_flags = fcntl(client_sock, F_GETFL, 0);
0194         fcntl(client_sock, F_SETFL, sock_flags | O_NONBLOCK);
0195     }
0196 
0197     ssh_channel forwarding_channel = ssh_channel_new(session);
0198     {
0199         const char *forward_remote_host = m_loopback ? "127.0.0.1" : m_host.constData();
0200         res = ssh_channel_open_forward(forwarding_channel, forward_remote_host, m_vncPort, "127.0.0.1", 0);
0201         if (res != SSH_OK || !ssh_channel_is_open(forwarding_channel))
0202         {
0203             qCDebug(KRDC) << "SSH channel open error" << ssh_get_error(session);
0204             return;
0205         }
0206         cleanup.forwarding_channel = forwarding_channel;
0207     }
0208 
0209     char client_read_buffer[40960];
0210     char *channel_read_buffer = nullptr;
0211     char *channel_read_buffer_ptr = nullptr;
0212     int channel_read_buffer_to_write;
0213     while (!m_stop_thread && !ssh_channel_is_eof(forwarding_channel)) {
0214         struct timeval timeout;
0215         timeout.tv_sec = 0;
0216         timeout.tv_usec = 200000;
0217 
0218         fd_set set;
0219         FD_ZERO(&set);
0220         FD_SET(client_sock, &set);
0221         ssh_channel channels[2] = { forwarding_channel, nullptr };
0222         ssh_channel channels_out[2] = { nullptr, nullptr };
0223 
0224         res = ssh_select(channels, channels_out, client_sock + 1, &set, &timeout);
0225         if (res == SSH_EINTR) continue;
0226         if (res == -1) break;
0227 
0228         bool error = false;
0229         if (FD_ISSET(client_sock, &set)) {
0230             int bytes_read;
0231             while (!error && (bytes_read = read(client_sock, client_read_buffer, sizeof client_read_buffer)) > 0) {
0232                 int bytes_written = 0;
0233                 int bytes_to_write = bytes_read;
0234                 for (char *ptr = client_read_buffer;
0235                      bytes_to_write > 0;
0236                      bytes_to_write -= bytes_written, ptr += bytes_written)
0237                 {
0238                     bytes_written = ssh_channel_write(forwarding_channel, ptr, bytes_to_write);
0239                     if (bytes_written <= 0) {
0240                         error = true;
0241                         qCDebug(KRDC) << "error on ssh_channel_write";
0242                         break;
0243                     }
0244                 }
0245             }
0246             if (bytes_read == 0) {
0247                 qCDebug(KRDC) << "error on tunnel read";
0248                 error = true;
0249             }
0250         }
0251 
0252         // If on the previous iteration we successfully wrote all we read, we need to read again
0253         if (!error && !channel_read_buffer) {
0254             const int bytes_available = ssh_channel_poll(forwarding_channel, 0);
0255             if (bytes_available == SSH_ERROR || bytes_available == SSH_EOF) {
0256                 qCDebug(KRDC) << "error on ssh_channel_poll";
0257                 error = true;
0258             } else if (bytes_available > 0) {
0259                 channel_read_buffer = new char[bytes_available];
0260                 channel_read_buffer_ptr = channel_read_buffer;
0261                 const int bytes_read = ssh_channel_read_nonblocking(forwarding_channel, channel_read_buffer, bytes_available, 0);
0262                 if (bytes_read <= 0) {
0263                     qCDebug(KRDC) << "error on ssh_channel_read_nonblocking";
0264                     error = true;
0265                 } else  {
0266                     channel_read_buffer_to_write = bytes_read;
0267                 }
0268             }
0269         }
0270 
0271         if (!error && channel_read_buffer) {
0272             for (int bytes_written = 0;
0273                  channel_read_buffer_to_write > 0;
0274                  channel_read_buffer_to_write -= bytes_written, channel_read_buffer_ptr += bytes_written)
0275             {
0276                 bytes_written = write(client_sock, channel_read_buffer_ptr, channel_read_buffer_to_write);
0277                 if (bytes_written == -1 && errno == EAGAIN) {
0278                     // socket is full, just carry on and we will write on the next iteration
0279                     // that is why the previous code does 'if (!channel_read_buffer)'
0280                     break;
0281                 }
0282                 if (bytes_written <= 0) {
0283                     qCDebug(KRDC) << "error on tunnel write";
0284                     error = true;
0285                     break;
0286                 }
0287             }
0288             if (channel_read_buffer_to_write <= 0) {
0289                 delete[] channel_read_buffer;
0290                 channel_read_buffer = nullptr;
0291             }
0292         }
0293     }
0294 
0295     delete[] channel_read_buffer;
0296     channel_read_buffer = nullptr;
0297 }