/*************************************************************************** * fqterm, a terminal emulator for both BBS and *nix. * * Copyright (C) 2008 fqterm development group. * * * * 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; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * ***************************************************************************/ #include "fqterm_ssh_socket.h" #include "fqterm_ssh_packet.h" #include "fqterm_ssh1_packet.h" #include "fqterm_ssh2_packet.h" #include "fqterm_ssh_kex.h" #include "fqterm_ssh2_kex.h" #include "fqterm_ssh_const.h" #include "fqterm_ssh_auth.h" #include "fqterm_ssh_channel.h" #include "fqterm_trace.h" #include "fqterm_path.h" #include "ssh_known_hosts.h" #include "ccan_base64.h" #include #include #include namespace FQTerm { #define V1STR "SSH-1.5-FQTermSSH\n" #define V2STR "SSH-2.0-FQTermSSH\n" #define SSH_V1_C "SSH-1.5-FQTermSSH" #define SSH_V2_C "SSH-2.0-FQTermSSH" FQTermSSHSocket::FQTermSSHSocket(int col, int row, const QString& termtype, const char *sshuser, const char *sshpasswd) : termtype_(termtype) { col_ = col; row_ = row; init_user_ = sshuser; init_passwd_ = sshpasswd; private_socket_ = new FQTermSocketPrivate(); buffer_init(&input_buffer); buffer_init(&output_buffer); buffer_init(&socket_buffer); packet_receiver_ = NULL; packet_sender_ = NULL; key_exchanger_ = NULL; authentication_ = NULL; ssh_channel_ = NULL; ssh_version_ = 1; is_channel_ok_ = false; auth_ok_emitted_ = false; FQ_VERIFY(connect(private_socket_, SIGNAL(hostFound()), this, SIGNAL(hostFound()))); FQ_VERIFY(connect(private_socket_, SIGNAL(connected()), this, SIGNAL(connected()))); FQ_VERIFY(connect(private_socket_, SIGNAL(connectionClosed()), this, SIGNAL(connectionClosed()))); FQ_VERIFY(connect(private_socket_, SIGNAL(delayedCloseFinished()), this, SIGNAL(delayedCloseFinished()))); FQ_VERIFY(connect(private_socket_, SIGNAL(error(QAbstractSocket::SocketError )), this, SIGNAL(error(QAbstractSocket::SocketError )))); FQ_VERIFY(connect(private_socket_, SIGNAL(socketState(int)), this, SIGNAL(socketState(int)))); FQ_VERIFY(connect(private_socket_, SIGNAL(readyRead()), this, SLOT(socketReadyRead()))); } FQTermSSHSocket::~FQTermSSHSocket() { delete private_socket_; buffer_deinit(&input_buffer); buffer_deinit(&output_buffer); buffer_deinit(&socket_buffer); delete packet_receiver_; delete packet_sender_; delete key_exchanger_; delete authentication_; delete ssh_channel_; } void FQTermSSHSocket::init(int ssh_version) { // Actually we could reuse these buffers, sender/receivers, and etc. // but in that case reset methods should be added to all these classes. // Guys lazy as me won't do that. buffer_clear(&input_buffer); buffer_clear(&output_buffer); buffer_clear(&socket_buffer); delete packet_receiver_; delete packet_sender_; delete key_exchanger_; delete authentication_; delete ssh_channel_; is_channel_ok_ = false; auth_ok_emitted_ = false; packet_sender_ = new FQTermSSHPacketSender(ssh_version); if (ssh_version == 1) { packet_receiver_ = new FQTermSSH1PacketReceiver; key_exchanger_ = new FQTermSSH1Kex(SSH_V1_C, server_name_.toLatin1().constData()); authentication_ = new FQTermSSH1PasswdAuth(init_user_, init_passwd_); ssh_channel_ = new FQTermSSH1Channel; } else { packet_receiver_ = new FQTermSSH2PacketReceiver; key_exchanger_ = new FQTermSSH2Kex(SSH_V2_C, server_name_.toLatin1().constData()); authentication_ = new FQTermSSH2PasswdAuth(init_user_, init_passwd_); ssh_channel_ = new FQTermSSH2Channel; } FQ_VERIFY(connect(packet_receiver_, SIGNAL(packetAvaliable(int)), this, SLOT(handlePacket(int)))); FQ_VERIFY(connect(packet_receiver_, SIGNAL(packetError(QString)), this, SLOT(handleError(QString)))); FQ_VERIFY(connect(packet_sender_, SIGNAL(dataToWrite()), this, SLOT(writeData()))); FQ_VERIFY(connect(key_exchanger_, SIGNAL(kexOK()), this, SLOT(kexOK()))); FQ_VERIFY(connect(key_exchanger_, SIGNAL(kexError(QString)), this, SLOT(handleError(QString)))); FQ_VERIFY(connect(key_exchanger_, SIGNAL(reKex()), packet_receiver_, SLOT(resetEncryption()))); FQ_VERIFY(connect(key_exchanger_, SIGNAL(reKex()), packet_sender_, SLOT(resetEncryption()))); FQ_VERIFY(connect(authentication_, SIGNAL(requestUserPwd(QString *, QString *, bool *)), this, SIGNAL(requestUserPwd(QString *, QString *, bool *)))); FQ_VERIFY(connect(authentication_, SIGNAL(authOK()), this, SLOT(authOK()))); FQ_VERIFY(connect(authentication_, SIGNAL(authError(QString)), this, SLOT(handleError(QString)))); FQ_VERIFY(connect(ssh_channel_, SIGNAL(channelOK()), this, SLOT(channelOK()))); FQ_VERIFY(connect(ssh_channel_, SIGNAL(channelReadyRead(const char *, int)), this, SLOT(channelReadyRead(const char *, int)))); if (ssh_version == 2) { FQ_VERIFY(connect(ssh_channel_, SIGNAL(channelError(QString)), this, SLOT(handleError(QString)))); FQ_VERIFY(connect(ssh_channel_, SIGNAL(channelClosed()), this, SIGNAL(connectionClosed()))); } key_exchanger_->initKex(packet_receiver_, packet_sender_); conn_info.proto = PROTO_SSH; conn_info.ssh_proto_info.proto_version = ssh_version; } void FQTermSSHSocket::kexOK() { FQ_TRACE("sshsocket", 3) << "Key exchange completed!"; conn_info.ssh_proto_info.c2s_cipher = packet_sender_->cipher->name; conn_info.ssh_proto_info.s2c_cipher = packet_receiver_->cipher->name; if (packet_sender_->mac) conn_info.ssh_proto_info.c2s_mac = packet_sender_->mac->name; if (packet_receiver_->mac) conn_info.ssh_proto_info.s2c_mac = packet_receiver_->mac->name; if (ssh_version_ == 2) { int nhosts; struct ssh_host *hosts; const char *hosts_file; #ifdef WIN32 QString known_hosts = getPath(USER_CONFIG) + "known_hosts"; hosts_file = strdup(known_hosts.toLatin1().constData()); #else hosts_file = ssh_hosts_filename(); #endif hosts = parse_hosts_file(hosts_file, &nhosts); int idx = find_ssh_host(hosts, nhosts, conn_info.hostname, conn_info.port); FQTermSSH2Kex *kex = dynamic_cast (key_exchanger_); bool isok; if (idx >=0) { if (key_matches(&hosts[idx], kex->K_S(), kex->K_S_len())) { conn_info.ssh_proto_info.key_matches = 1; } else { conn_info.ssh_proto_info.key_matches = 0; emit warnInsecure("Host key mismatch!", &isok); if (!isok) handleError("Uesr closed connection due to key mismatch."); } } else { conn_info.ssh_proto_info.key_matches = 0; unsigned char hash[32]; char hash_b64[64]; kex->hostKeyHash(hash); base64_encode(hash_b64, sizeof(hash_b64), (const char*)hash, 32); hash_b64[base64_encoded_length(32)-1] = 0; /* final byte is '=' */ const char *tab = "0123456789ABCDEF"; QString msg = "Unknown host, trust the host key?\nSHA256: "; for (int i = 0; i < 32; i++) { char d[4]; d[0] = tab[hash[i]>>4]; d[1] = tab[hash[i]&0xf]; d[2] = (i==31)?'\n':':'; d[3] = 0; msg.append(d); } msg.append(QString("base64: %1").arg(hash_b64)); emit warnInsecure(msg, &isok); if (!isok) handleError("Uesr closed connection due to an unknown host."); else /* FIXME: port != 22? */ append_hostkey(hosts_file, conn_info.hostname, kex->K_S(), kex->K_S_len()); } #ifdef WIN32 free(hosts_file); #endif free(hosts); } key_exchanger_->hostKeyHash(conn_info.ssh_proto_info.hash); authentication_->initAuth(packet_receiver_, packet_sender_); } void FQTermSSHSocket::authOK() { FQ_TRACE("sshsocket", 3) << "Auth completed!"; ssh_channel_->initChannel(packet_receiver_, packet_sender_, col_, row_, termtype_); } void FQTermSSHSocket::channelOK() { FQ_TRACE("sshsocket", 3) << "Channel established!"; is_channel_ok_ = true; //auth_ok_emitted_ = false; } void FQTermSSHSocket::channelReadyRead(const char *data, int len) { buffer_append(&input_buffer, (const uint8_t*)data, len); emit readyRead(); } unsigned long FQTermSSHSocket::socketWriteBlock(const char *data, unsigned long len) { QByteArray to_write(data, len); return private_socket_->writeBlock(to_write); } void FQTermSSHSocket::socketReadyRead() { if (!auth_ok_emitted_ && is_channel_ok_) { auth_ok_emitted_ = true; emit sshAuthOK(); } unsigned long size; switch (ssh_socket_state_) { case BeforeSession: { QByteArray str; int version; size = private_socket_->bytesAvailable(); str = private_socket_->readBlock(size); server_name_ = QString(str).trimmed(); // remove the newline version = chooseVersion(str.data()); FQ_TRACE("sshsocket", 3) << "SSH server: " << server_name_; FQ_TRACE("sshsocket", 3) << "SSH version chosen: " << version; if (version == 1) { bool isok; emit warnInsecure(tr("You are using the insecure SSH1 connection, continue?"), &isok); if (!isok) { handleError(tr("Closed by user because of the insecure SSH1 connection.")); return; } init(1); ssh_version_ = 1; socketWriteBlock(V1STR, strlen(V1STR)); } else if (version == 2) { init(2); ssh_version_ = 2; socketWriteBlock(V2STR, strlen(V2STR)); } else { handleError(tr("Unknown SSH version. " "Check if you set the right server and port.")); return ; } ssh_socket_state_ = SockSession; private_socket_->flush(); break; } case SockSession: parsePacket(); } } void FQTermSSHSocket::parsePacket() { unsigned long size; QByteArray data; size = private_socket_->bytesAvailable(); data = private_socket_->readBlock(size); buffer_append(&socket_buffer, (const uint8_t*)data.data(), size); packet_receiver_->parseData(&socket_buffer); } int FQTermSSHSocket::chooseVersion(const QString &ver) { QString version = ver.mid(ver.indexOf("-") + 1); version = version.left(version.indexOf("-")); if (version == "2.0" || version == "1.99") return 2; else if (version == "1.3" || version == "1.5") return 1; else return -1; } void FQTermSSHSocket::connectToHost(const QString &host_name, quint16 port) { ssh_socket_state_ = BeforeSession; private_socket_->connectToHost(host_name, port); strncpy(conn_info.hostname, host_name.toLatin1().constData(), sizeof(conn_info.hostname)); conn_info.port = port; } void FQTermSSHSocket::writeData() { socketWriteBlock((const char*)buffer_data(&packet_sender_->data_to_send), buffer_len(&packet_sender_->data_to_send)); private_socket_->flush(); } void FQTermSSHSocket::handlePacket(int type) { if (ssh_version_ == 1) { switch (type) { case SSH1_MSG_DISCONNECT: char *reason; reason = (char*)packet_receiver_->getString(); FQ_TRACE("sshsocket", 1) << "Disconnect because: " << reason; delete [] reason; break; default: return ; } } else { // ssh_version_ == 2; switch (type) { case SSH2_MSG_DISCONNECT: char *reason; packet_receiver_->consume(4); reason = (char*)packet_receiver_->getString(); FQ_TRACE("sshsocket", 1) << "Disconnect because: " << reason; delete[] reason; break; default: return ; } } } unsigned long FQTermSSHSocket::bytesAvailable() { return buffer_len(&input_buffer); } QByteArray FQTermSSHSocket::readBlock(unsigned long size) { QByteArray data(size, 0); buffer_get(&input_buffer, (uint8_t*)data.data(), size); return data; } long FQTermSSHSocket::writeBlock(const QByteArray &data) { if (!is_channel_ok_) return 0; unsigned long size = data.size(); buffer_append(&output_buffer, (const uint8_t*)data.data(), size); return size; } void FQTermSSHSocket::flush() { if (!is_channel_ok_) return; size_t size = buffer_len(&output_buffer); ssh_channel_->sendData((const char *)buffer_data(&output_buffer), size); buffer_consume(&output_buffer, size); } void FQTermSSHSocket::close() { private_socket_->close(); } void FQTermSSHSocket::handleError(QString reason) { close(); emit errorMessage(reason); emit connectionClosed(); } void FQTermSSHSocket::setProxy(int nProxyType, bool bAuth, const QString &strProxyHost, quint16 uProxyPort, const QString &strProxyUsr, const QString &strProxyPwd) { private_socket_->setProxy(nProxyType, bAuth, strProxyHost, uProxyPort, strProxyUsr, strProxyPwd); } bool FQTermSSHSocket::setTermSize(int col, int row) { if (ssh_channel_ && is_channel_ok_) ssh_channel_->changeTermSize(col, row); return true; } } // namespace FQTerm #include "fqterm_ssh_socket.moc"