File indexing completed on 2024-04-21 04:58:36

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