You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1105 lines
41 KiB
1105 lines
41 KiB
/***************************************************************************
|
|
ksshprocess.cpp - description
|
|
-------------------
|
|
begin : Tue Jul 31 2001
|
|
copyright : (C) 2001 by Lucas Fisher
|
|
email : ljfisher@purdue.edu
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* 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. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
/*
|
|
* See the KSshProcess header for examples on use.
|
|
*
|
|
* This class uses a hacked version of the PTYProcess
|
|
* class. This was needed because the kdelibs PTYProcess does not provide
|
|
* access to the pty file descriptor which we need, because ssh prints the
|
|
* password prompt to the pty and reads the password from the pty. I don't
|
|
* feel I know enough about ptys to confidently modify the orignial
|
|
* PTYProcess class.
|
|
*
|
|
* To start ssh we take the arguments the user gave us
|
|
* in the SshOptList and build the ssh command arguments based on the version
|
|
* of ssh we are using. This command and its arguments are passed to
|
|
* PTYProcess for execution. Once ssh is started we scan each line of input
|
|
* from stdin, stderr, and the pty for recognizable strings. The recognizable
|
|
* strings are taken from several string tables. Each table contains a string
|
|
* for each specific version of ssh we support and a string for a generic
|
|
* version of OpenSSH and commercial SSH incase we don't recognized the
|
|
* specific ssh version strings (as when a new SSH version is released after
|
|
* a release of KSshProcess). There are tables for ssh version strings,
|
|
* password prompts, new host key errors, different host key errors,
|
|
* messages than indicate a successful connect, authentication errors, etc.
|
|
* If we find user interaction is necessary, for instance to provide a
|
|
* password or passphrase, we return a err code to the user who can send
|
|
* a message to KSshProcess, using one of several methods, to correct
|
|
* the error.
|
|
*
|
|
* Determining when the ssh connection has successfully authenticationed has
|
|
* proved to be the most difficult challenge. OpenSSH does not print a message
|
|
* on successful authentication, thus the only way to know is to send data
|
|
* and wait for a return. The problem here is sometimes it can take a bit
|
|
* to establish the connection (for example, do to DNS lookups). This means
|
|
* the user may be sitting there waiting for a connection that failed.
|
|
* Instead, ssh is always started with the verbose flag. Then we look for
|
|
* a message that indicates auth succeeded. This is hazardous because
|
|
* debug messages are more likely to change between OpenSSH releases.
|
|
* Thus, we could become incompatible with new OpenSSH releases.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include "ksshprocess.h"
|
|
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
|
|
#ifdef HAVE_SYS_TIME_H
|
|
#include <sys/time.h>
|
|
#endif
|
|
|
|
#include <kstandarddirs.h>
|
|
#include <klocale.h>
|
|
#include <tqregexp.h>
|
|
|
|
/*
|
|
* The following are tables of string and regexps we match
|
|
* against the output of ssh. An entry in each array
|
|
* corresponds the the version of ssh found in versionStrs[].
|
|
*
|
|
* The version strings must be ordered in the array from most
|
|
* specific to least specific in cases where the beginning
|
|
* of several version strings are the similar. For example,
|
|
* consider the openssh version strings. The generic "OpenSSH"
|
|
* must be the last of the openssh version strings in the array
|
|
* so that is matched last. We use these generic version strings
|
|
* so we can do a best effor to support unknown ssh versions.
|
|
*/
|
|
TQRegExp KSshProcess::versionStrs[] = {
|
|
TQRegExp("OpenSSH_3\\.[6-9]|OpenSSH_[1-9]*[4-9]\\.[0-9]"),
|
|
TQRegExp("OpenSSH"),
|
|
TQRegExp("SSH Secure Shell")
|
|
};
|
|
|
|
const char * const KSshProcess::passwordPrompt[] = {
|
|
"password:", // OpenSSH
|
|
"password:", // OpenSSH
|
|
"password:" // SSH
|
|
};
|
|
|
|
const char * const KSshProcess::passphrasePrompt[] = {
|
|
"Enter passphrase for key",
|
|
"Enter passphrase for key",
|
|
"Passphrase for key"
|
|
};
|
|
|
|
const char * const KSshProcess::authSuccessMsg[] = {
|
|
"Authentication succeeded",
|
|
"ssh-userauth2 successful",
|
|
"Received SSH_CROSS_AUTHENTICATED packet"
|
|
};
|
|
|
|
const char* const KSshProcess::authFailedMsg[] = {
|
|
"Permission denied (",
|
|
"Permission denied (",
|
|
"Authentication failed."
|
|
};
|
|
|
|
const char* const KSshProcess::tryAgainMsg[] = {
|
|
"please try again",
|
|
"please try again",
|
|
"adjfhjsdhfdsjfsjdfhuefeufeuefe"
|
|
};
|
|
|
|
TQRegExp KSshProcess::hostKeyMissingMsg[] = {
|
|
TQRegExp("The authenticity of host|No (DSA|RSA) host key is known for"),
|
|
TQRegExp("The authenticity of host|No (DSA|RSA) host key is known for"),
|
|
TQRegExp("Host key not found from database")
|
|
};
|
|
|
|
const char* const KSshProcess::continuePrompt[] = {
|
|
"Are you sure you want to continue connecting (yes/no)?",
|
|
"Are you sure you want to continue connecting (yes/no)?",
|
|
"Are you sure you want to continue connecting (yes/no)?"
|
|
};
|
|
|
|
const char* const KSshProcess::hostKeyChangedMsg[] = {
|
|
"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!",
|
|
"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!",
|
|
"WARNING: HOST IDENTIFICATION HAS CHANGED!"
|
|
};
|
|
|
|
TQRegExp KSshProcess::keyFingerprintMsg[] = {
|
|
TQRegExp("..(:..){15}"),
|
|
TQRegExp("..(:..){15}"),
|
|
TQRegExp(".....(-.....){10}")
|
|
};
|
|
|
|
TQRegExp KSshProcess::knownHostsFileMsg[] = {
|
|
TQRegExp("Add correct host key in (.*) to get rid of this message."),
|
|
TQRegExp("Add correct host key in (.*) to get rid of this message."),
|
|
TQRegExp("Add correct host key to \"(.*)\"")
|
|
};
|
|
|
|
|
|
// This prompt only applies to commerical ssh.
|
|
const char* const KSshProcess::changeHostKeyOnDiskPrompt[] = {
|
|
"as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf",
|
|
"as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf",
|
|
"Do you want to change the host key on disk (yes/no)?"
|
|
};
|
|
|
|
// We need this in addition the authFailedMsg because when
|
|
// OpenSSH gets a changed host key it will fail to connect
|
|
// depending on the StrictHostKeyChecking option. Depending
|
|
// how this option is set, it will print "Permission denied"
|
|
// and quit, or print "Host key verification failed." and
|
|
// quit. The later if StrictHostKeyChecking is "no".
|
|
// The former if StrictHostKeyChecking is
|
|
// "yes" or explicitly set to "ask".
|
|
TQRegExp KSshProcess::hostKeyVerifyFailedMsg[] = {
|
|
TQRegExp("Host key verification failed\\."),
|
|
TQRegExp("Host key verification failed\\."),
|
|
TQRegExp("Disconnected; key exchange or algorithm? negotiation failed \\(Key exchange failed\\.\\)\\.")
|
|
};
|
|
|
|
const char * const KSshProcess::connectionClosedMsg[] = {
|
|
"Connection closed by remote host",
|
|
"Connection closed by remote host",
|
|
"Connection closed by remote host"
|
|
};
|
|
|
|
|
|
void KSshProcess::SIGCHLD_handler(int) {
|
|
while(waitpid(-1, NULL, WNOHANG) > 0);
|
|
}
|
|
|
|
void KSshProcess::installSignalHandlers() {
|
|
struct sigaction act;
|
|
memset(&act,0,sizeof(act));
|
|
act.sa_handler = SIGCHLD_handler;
|
|
act.sa_flags = 0
|
|
#ifdef SA_NOCLDSTOP
|
|
| SA_NOCLDSTOP
|
|
#endif
|
|
#ifdef SA_RESTART
|
|
| SA_RESTART
|
|
#endif
|
|
;
|
|
sigaction(SIGCHLD,&act,NULL);
|
|
}
|
|
|
|
void KSshProcess::removeSignalHandlers() {
|
|
struct sigaction act;
|
|
memset(&act,0,sizeof(act));
|
|
act.sa_handler = SIG_DFL;
|
|
sigaction(SIGCHLD,&act,NULL);
|
|
}
|
|
|
|
KSshProcess::KSshProcess()
|
|
: mVersion(UNKNOWN_VER), mConnected(false),
|
|
mRunning(false), mConnectState(0) {
|
|
mSshPath = KStandardDirs::findExe(TQString::fromLatin1("ssh"));
|
|
kdDebug(KSSHPROC) << "KSshProcess::KSshProcess(): ssh path [" <<
|
|
mSshPath << "]" << endl;
|
|
|
|
installSignalHandlers();
|
|
}
|
|
|
|
KSshProcess::KSshProcess(TQString pathToSsh)
|
|
: mSshPath(pathToSsh), mVersion(UNKNOWN_VER), mConnected(false),
|
|
mRunning(false), mConnectState(0) {
|
|
installSignalHandlers();
|
|
}
|
|
|
|
KSshProcess::~KSshProcess(){
|
|
disconnect();
|
|
removeSignalHandlers();
|
|
while(waitpid(-1, NULL, WNOHANG) > 0);
|
|
}
|
|
|
|
bool KSshProcess::setSshPath(TQString pathToSsh) {
|
|
mSshPath = pathToSsh;
|
|
version();
|
|
if( mVersion == UNKNOWN_VER )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
KSshProcess::SshVersion KSshProcess::version() {
|
|
TQString cmd;
|
|
cmd = mSshPath+" -V 2>&1";
|
|
|
|
// Get version string from ssh client.
|
|
FILE *p;
|
|
if( (p = popen(cmd.latin1(), "r")) == NULL ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::version(): "
|
|
"failed to start ssh: " << strerror(errno) << endl;
|
|
return UNKNOWN_VER;
|
|
}
|
|
|
|
// Determine of the version from the version string.
|
|
size_t len;
|
|
char buf[128];
|
|
if( (len = fread(buf, sizeof(char), sizeof(buf)-1, p)) == 0 ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::version(): "
|
|
"Read of ssh version string failed " <<
|
|
strerror(ferror(p)) << endl;
|
|
return UNKNOWN_VER;
|
|
}
|
|
if( pclose(p) == -1 ) {
|
|
kdError(KSSHPROC) << "KSshProcess::version(): pclose failed." << endl;
|
|
}
|
|
buf[len] = '\0';
|
|
TQString ver;
|
|
ver = buf;
|
|
kdDebug(KSSHPROC) << "KSshProcess::version(): "
|
|
"got version string [" << ver << "]" << endl;
|
|
|
|
mVersion = UNKNOWN_VER;
|
|
for(int i = 0; i < SSH_VER_MAX; i++) {
|
|
if( ver.find(versionStrs[i]) != -1 ) {
|
|
mVersion = (SshVersion)i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
kdDebug(KSSHPROC) << "KSshPRocess::version(): version number = "
|
|
<< mVersion << endl;
|
|
|
|
if( mVersion == UNKNOWN_VER ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::version(): "
|
|
"Sorry, I don't know about this version of ssh" << endl;
|
|
mError = ERR_UNKNOWN_VERSION;
|
|
return UNKNOWN_VER;
|
|
}
|
|
|
|
return mVersion;
|
|
}
|
|
/*
|
|
TQString KSshProcess::versionStr() {
|
|
if( mVersion == UNKNOWN_VER ) {
|
|
version();
|
|
if( mVersion == UNKNOWN_VER )
|
|
return TQString::null;
|
|
}
|
|
|
|
return TQString::fromLatin1(versionStrs[mVersion]);
|
|
}
|
|
*/
|
|
|
|
bool KSshProcess::setOptions(const SshOptList& opts) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::setOptions()" << endl;
|
|
mArgs.clear();
|
|
SshOptListConstIterator it;
|
|
TQString cmd, subsystem;
|
|
mPassword = mUsername = mHost = TQString::null;
|
|
TQCString tmp;
|
|
for(it = opts.begin(); it != opts.end(); ++it) {
|
|
//kdDebug(KSSHPROC) << "opt.opt = " << (*it).opt << endl;
|
|
//kdDebug(KSSHPROC) << "opt.str = " << (*it).str << endl;
|
|
//kdDebug(KSSHPROC) << "opt.num = " << (*it).num << endl;
|
|
switch( (*it).opt ) {
|
|
case SSH_VERBOSE:
|
|
mArgs.append("-v");
|
|
break;
|
|
|
|
case SSH_SUBSYSTEM:
|
|
subsystem = (*it).str;
|
|
break;
|
|
|
|
case SSH_PORT:
|
|
mArgs.append("-p");
|
|
tmp.setNum((*it).num);
|
|
mArgs.append(tmp);
|
|
mPort = (*it).num;
|
|
break;
|
|
|
|
case SSH_HOST:
|
|
mHost = (*it).str;
|
|
break;
|
|
|
|
case SSH_USERNAME:
|
|
mArgs.append("-l");
|
|
mArgs.append((*it).str.latin1());
|
|
mUsername = (*it).str;
|
|
break;
|
|
|
|
case SSH_PASSWD:
|
|
mPassword = (*it).str;
|
|
break;
|
|
|
|
case SSH_PROTOCOL:
|
|
if( mVersion <= OPENSSH ) {
|
|
tmp = "Protocol=";
|
|
tmp += TQString::number((*it).num).latin1();
|
|
mArgs.append("-o");
|
|
mArgs.append(tmp);
|
|
}
|
|
else if( mVersion <= SSH ) {
|
|
if( (*it).num == 1 ) {
|
|
mArgs.append("-1");
|
|
}
|
|
// else uses version 2 by default
|
|
}
|
|
break;
|
|
|
|
case SSH_FORWARDX11:
|
|
tmp = "ForwardX11=";
|
|
tmp += (*it).boolean ? "yes" : "no";
|
|
mArgs.append("-o");
|
|
mArgs.append(tmp);
|
|
break;
|
|
|
|
case SSH_FORWARDAGENT:
|
|
tmp = "ForwardAgent=";
|
|
tmp += (*it).boolean ? "yes" : "no";
|
|
mArgs.append("-o");
|
|
mArgs.append(tmp);
|
|
break;
|
|
|
|
case SSH_ESCAPE_CHAR:
|
|
if( (*it).num == -1 )
|
|
tmp = "none";
|
|
else
|
|
tmp = (char)((*it).num);
|
|
mArgs.append("-e");
|
|
mArgs.append(tmp);
|
|
break;
|
|
|
|
case SSH_OPTION:
|
|
// don't allow NumberOfPasswordPrompts or StrictHostKeyChecking
|
|
// since KSshProcess depends on specific setting of these for
|
|
// preforming authentication correctly.
|
|
tmp = (*it).str.latin1();
|
|
if( tmp.contains("NumberOfPasswordPrompts") ||
|
|
tmp.contains("StrictHostKeyChecking") ) {
|
|
mError = ERR_INVALID_OPT;
|
|
return false;
|
|
}
|
|
else {
|
|
mArgs.append("-o");
|
|
mArgs.append(tmp);
|
|
}
|
|
break;
|
|
|
|
case SSH_COMMAND:
|
|
cmd = (*it).str;
|
|
break;
|
|
|
|
default:
|
|
kdDebug(KSSHPROC) << "KSshProcess::setOptions(): "
|
|
"unrecognized ssh opt " << (*it).opt << endl;
|
|
}
|
|
}
|
|
|
|
if( !subsystem.isEmpty() && !cmd.isEmpty() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::setOptions(): "
|
|
"cannot use a subsystem and command at the same time" << endl;
|
|
mError = ERR_CMD_SUBSYS_CONFLICT;
|
|
mErrorMsg = i18n("Cannot specify a subsystem and command at the same time.");
|
|
return false;
|
|
}
|
|
|
|
// These options govern the behavior of ssh and
|
|
// cannot be defined by the user
|
|
//mArgs.append("-o");
|
|
//mArgs.append("StrictHostKeyChecking=ask");
|
|
mArgs.append("-v"); // So we get a message that the
|
|
// connection was successful
|
|
if( mVersion <= OPENSSH ) {
|
|
// nothing
|
|
}
|
|
else if( mVersion <= SSH ) {
|
|
mArgs.append("-o"); // So we can check if the connection was successful
|
|
mArgs.append("AuthenticationSuccessMsg=yes");
|
|
}
|
|
|
|
if( mHost.isEmpty() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::setOptions(): "
|
|
"a host name must be supplied" << endl;
|
|
return false;
|
|
}
|
|
else {
|
|
mArgs.append(mHost.latin1());
|
|
}
|
|
|
|
if( !subsystem.isEmpty() ) {
|
|
mArgs.append("-s");
|
|
mArgs.append(subsystem.latin1());
|
|
}
|
|
|
|
if( !cmd.isEmpty() ) {
|
|
mArgs.append(cmd.latin1());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void KSshProcess::printArgs() {
|
|
TQValueListIterator<TQCString> it;
|
|
for( it = mArgs.begin(); it != mArgs.end(); ++it) {
|
|
kdDebug(KSSHPROC) << "arg: " << *it << endl;
|
|
}
|
|
}
|
|
|
|
|
|
int KSshProcess::error(TQString& msg) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::error()" << endl;
|
|
kdDebug() << mErrorMsg << endl;
|
|
msg = mErrorMsg;
|
|
return mError;
|
|
}
|
|
|
|
void KSshProcess::kill(int signal) {
|
|
int pid = ssh.pid();
|
|
|
|
kdDebug(KSSHPROC) << "KSshProcess::kill(signal:" << signal
|
|
<< "): ssh pid is " << pid << endl;
|
|
kdDebug(KSSHPROC) << "KSshPRocess::kill(): we are "
|
|
<< (mConnected ? "" : "not ") << "connected" << endl;
|
|
kdDebug(KSSHPROC) << "KSshProcess::kill(): we are "
|
|
<< (mRunning ? "" : "not ") << "running a ssh process" << endl;
|
|
|
|
if( mRunning && pid > 1 ) {
|
|
// Kill the child process...
|
|
if ( ::kill(pid, signal) == 0 ) {
|
|
// clean up if we tried to kill the process
|
|
if( signal == SIGTERM || signal == SIGKILL ) {
|
|
while(waitpid(-1, NULL, WNOHANG) > 0);
|
|
mConnected = false;
|
|
mRunning = false;
|
|
}
|
|
}
|
|
else
|
|
kdDebug(KSSHPROC) << "KSshProcess::kill(): kill failed" << endl;
|
|
}
|
|
else
|
|
kdDebug(KSSHPROC) << "KSshProcess::kill(): "
|
|
"Refusing to kill ssh process" << endl;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Try to open an ssh connection.
|
|
* SSH prints certain messages to certain file descriptiors:
|
|
* passwordPrompt - pty
|
|
* passphrasePrompt - pty
|
|
* authSuccessMsg - stderr (OpenSSH),
|
|
* authFailedMsg - stderr
|
|
* hostKeyMissing - stderr
|
|
* hostKeyChanged - stderr
|
|
* continuePrompt - stderr
|
|
*
|
|
* We will use a select to wait for a line on each descriptor. Then get
|
|
* each line that available and take action based on it. The type
|
|
* of messages we are looking for and the action we take on each
|
|
* message are:
|
|
* passwordPrompt - Return false, set error to ERR_NEED_PASSWD.
|
|
* On the next call to connect() we expect a password
|
|
* to be available.
|
|
*
|
|
* passpharsePrompt - Return false, set error to ERR_NEED_PASSPHRASE.
|
|
* On the next call to connect() we expect a
|
|
* passphrase to be available.
|
|
*
|
|
* authSuccessMsg - Return true, as we have successfully established a
|
|
* ssh connection.
|
|
*
|
|
* authFailedMsg - Return false, set error to ERR_AUTH_FAILED. We
|
|
* were unable to authenticate the connection given
|
|
* the available authentication information.
|
|
*
|
|
* hostKeyMissing - Return false, set error to ERR_NEW_HOST_KEY. Caller
|
|
* must call KSshProcess.acceptHostKey(bool) to accept
|
|
* or reject the key before calling connect() again.
|
|
*
|
|
* hostKeyChanged - Return false, set error to ERR_DIFF_HOST_KEY. Caller
|
|
* must call KSshProcess.acceptHostKey(bool) to accept
|
|
* or reject the key before calling connect() again.
|
|
*
|
|
* continuePrompt - Send 'yes' or 'no' to accept or reject a key,
|
|
* respectively.
|
|
*
|
|
*/
|
|
|
|
|
|
void KSshProcess::acceptHostKey(bool accept) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::acceptHostKey(accept:"
|
|
<< accept << ")" << endl;
|
|
mAcceptHostKey = accept;
|
|
}
|
|
|
|
void KSshProcess::setPassword(TQString password) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::setPassword(password:xxxxxxxx)" << endl;
|
|
mPassword = password;
|
|
}
|
|
|
|
TQString KSshProcess::getLine() {
|
|
static TQStringList buffer;
|
|
TQString line = TQString::null;
|
|
TQCString ptyLine, errLine;
|
|
|
|
if( buffer.empty() ) {
|
|
// PtyProcess buffers lines. First check that there
|
|
// isn't something on the PtyProces buffer or that there
|
|
// is not data ready to be read from the pty or stderr.
|
|
ptyLine = ssh.readLineFromPty(false);
|
|
errLine = ssh.readLineFromStderr(false);
|
|
|
|
// If PtyProcess did have something for us, get it and
|
|
// place it in our line buffer.
|
|
if( ! ptyLine.isEmpty() ) {
|
|
buffer.prepend(TQString(ptyLine));
|
|
}
|
|
|
|
if( ! errLine.isEmpty() ) {
|
|
buffer.prepend(TQString(errLine));
|
|
}
|
|
|
|
// If we still don't have anything in our buffer so there must
|
|
// not be anything on the pty or stderr. Setup a select()
|
|
// to wait for some data from SSH.
|
|
if( buffer.empty() ) {
|
|
//kdDebug(KSSHPROC) << "KSshProcess::getLine(): " <<
|
|
// "Line buffer empty, calling select() to wait for data." << endl;
|
|
int errfd = ssh.stderrFd();
|
|
int ptyfd = ssh.fd();
|
|
fd_set rfds;
|
|
fd_set efds;
|
|
struct timeval tv;
|
|
|
|
// find max file descriptor
|
|
int maxfd = ptyfd > errfd ? ptyfd : errfd;
|
|
|
|
FD_ZERO(&rfds);
|
|
FD_SET(ptyfd, &rfds); // Add pty file descriptor
|
|
FD_SET(errfd, &rfds); // Add std error file descriptor
|
|
|
|
FD_ZERO(&efds);
|
|
FD_SET(ptyfd, &efds);
|
|
FD_SET(errfd, &efds);
|
|
|
|
tv.tv_sec = 60; tv.tv_usec = 0; // 60 second timeout
|
|
|
|
// Wait for a message from ssh on stderr or the pty.
|
|
int ret = -1;
|
|
do
|
|
ret = ::select(maxfd+1, &rfds, NULL, &efds, &tv);
|
|
while( ret == -1 && errno == EINTR );
|
|
|
|
// Handle any errors from select
|
|
if( ret == 0 ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): " <<
|
|
"timed out waiting for a response" << endl;
|
|
mError = ERR_TIMED_OUT;
|
|
return TQString::null;
|
|
}
|
|
else if( ret == -1 ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
<< "select error: " << strerror(errno) << endl;
|
|
mError = ERR_INTERNAL;
|
|
return TQString::null;
|
|
}
|
|
|
|
// We are not respecting any type of order in which the
|
|
// lines were received. Who knows whether pty or stderr
|
|
// had data on it first.
|
|
if( FD_ISSET(ptyfd, &rfds) ) {
|
|
ptyLine = ssh.readLineFromPty(false);
|
|
buffer.prepend(TQString(ptyLine));
|
|
//kdDebug(KSSHPROC) << "KSshProcess::getLine(): "
|
|
// "line from pty -" << ptyLine << endl;
|
|
}
|
|
|
|
if( FD_ISSET(errfd, &rfds) ) {
|
|
errLine = ssh.readLineFromStderr(false);
|
|
buffer.prepend(TQString(errLine));
|
|
//kdDebug(KSSHPROC) << "KSshProcess::getLine(): "
|
|
// "line from err -" << errLine << endl;
|
|
}
|
|
|
|
if( FD_ISSET(ptyfd, &efds) ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::getLine(): "
|
|
"Exception on pty file descriptor." << endl;
|
|
}
|
|
|
|
if( FD_ISSET(errfd, &efds) ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::getLine(): "
|
|
"Exception on std err file descriptor." << endl;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// We should have something in our buffer now.
|
|
// Return the last line.
|
|
//it = buffer.end();
|
|
//line = *it;
|
|
//buffer.remove(it);
|
|
|
|
line = buffer.last();
|
|
buffer.pop_back();
|
|
|
|
if( line.isNull() && buffer.count() > 0 ) {
|
|
line = buffer.last();
|
|
buffer.pop_back();
|
|
}
|
|
|
|
// kdDebug(KSSHPROC) << "KSshProcess::getLine(): " <<
|
|
// buffer.count() << " lines in buffer" << endl;
|
|
kdDebug(KSSHPROC) << "KSshProcess::getLine(): "
|
|
"ssh: " << line << endl;
|
|
|
|
|
|
return line;
|
|
}
|
|
|
|
// All the different states we could go through while trying to connect.
|
|
enum sshConnectState {
|
|
STATE_START, STATE_TRY_PASSWD, STATE_WAIT_PROMPT, STATE_NEW_KEY_CONTINUE,
|
|
STATE_DIFF_KEY_CONTINUE, STATE_FATAL, STATE_WAIT_CONTINUE_PROMPT,
|
|
STATE_SEND_CONTINUE, STATE_AUTH_FAILED, STATE_NEW_KEY_WAIT_CONTINUE,
|
|
STATE_DIFF_KEY_WAIT_CONTINUE, STATE_TRY_PASSPHRASE
|
|
};
|
|
|
|
// Print the state as a string. Good for debugging
|
|
const char* stateStr(int state) {
|
|
switch(state) {
|
|
case STATE_START:
|
|
return "STATE_START";
|
|
case STATE_TRY_PASSWD:
|
|
return "STATE_TRY_PASSWD";
|
|
case STATE_WAIT_PROMPT:
|
|
return "STATE_WAIT_PROMPT";
|
|
case STATE_NEW_KEY_CONTINUE:
|
|
return "STATE_NEW_KEY_CONTINUE";
|
|
case STATE_DIFF_KEY_CONTINUE:
|
|
return "STATE_DIFF_KEY_CONTINUE";
|
|
case STATE_FATAL:
|
|
return "STATE_FATAL";
|
|
case STATE_WAIT_CONTINUE_PROMPT:
|
|
return "STATE_WAIT_CONTINUE_PROMPT";
|
|
case STATE_SEND_CONTINUE:
|
|
return "STATE_SEND_CONTINE";
|
|
case STATE_AUTH_FAILED:
|
|
return "STATE_AUTH_FAILED";
|
|
case STATE_NEW_KEY_WAIT_CONTINUE:
|
|
return "STATE_NEW_KEY_WAIT_CONTINUE";
|
|
case STATE_DIFF_KEY_WAIT_CONTINUE:
|
|
return "STATE_DIFF_KEY_WAIT_CONTINUE";
|
|
case STATE_TRY_PASSPHRASE:
|
|
return "STATE_TRY_PASSPHRASE";
|
|
}
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
bool KSshProcess::connect() {
|
|
if( mVersion == UNKNOWN_VER ) {
|
|
// we don't know the ssh version yet, so find out
|
|
version();
|
|
if( mVersion == -1 ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// We'll put a limit on the number of state transitions
|
|
// to ensure we don't go out of control.
|
|
int transitionLimit = 500;
|
|
|
|
while(--transitionLimit) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
<< "Connect state " << stateStr(mConnectState) << endl;
|
|
|
|
TQString line; // a line from ssh
|
|
TQString msgBuf; // buffer for important messages from ssh
|
|
// which are to be returned to the user
|
|
|
|
switch(mConnectState) {
|
|
// STATE_START:
|
|
// Executes the ssh binary with the options provided. If no options
|
|
// have been specified, sets error and returns false. Continue to
|
|
// state 1 if execution is successful, otherwise set error and
|
|
// return false.
|
|
case STATE_START:
|
|
// reset some key values to safe values
|
|
mAcceptHostKey = false;
|
|
mKeyFingerprint = TQString::null;
|
|
mKnownHostsFile = TQString::null;
|
|
|
|
if( mArgs.isEmpty() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): ssh options "
|
|
"need to be set first using setArgs()" << endl;
|
|
mError = ERR_NO_OPTIONS;
|
|
mErrorMsg = i18n("No options provided for ssh execution.");
|
|
return false;
|
|
}
|
|
|
|
if( ssh.exec(mSshPath.latin1(), mArgs) ) {
|
|
kdDebug(KSSHPROC) <<
|
|
"KSshProcess::connect(): ssh exec failed" << endl;
|
|
mError = ERR_CANNOT_LAUNCH;
|
|
mErrorMsg = i18n("Failed to execute ssh process.");
|
|
return false;
|
|
}
|
|
|
|
kdDebug(KSSHPROC) << "KSshPRocess::connect(): ssh pid = " << ssh.pid() << endl;
|
|
|
|
// set flag to indicate what have started a ssh process
|
|
mRunning = true;
|
|
mConnectState = STATE_WAIT_PROMPT;
|
|
break;
|
|
|
|
// STATE_WAIT_PROMPT:
|
|
// Get a line of input from the ssh process. Check the contents
|
|
// of the line to determine the next state. Ignore the line
|
|
// if we don't recognize its contents. If the line contains
|
|
// the continue prompt, we have an error since we should never
|
|
// get that line in this state. Set ERR_INVALID_STATE error
|
|
// and return false.
|
|
case STATE_WAIT_PROMPT:
|
|
line = getLine();
|
|
if( line.isNull() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"Got null line in STATE_WAIT_PROMPT." << endl;
|
|
mError = ERR_INTERACT;
|
|
mErrorMsg =
|
|
i18n("Error encountered while talking to ssh.");
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
else if( line.find(TQString::fromLatin1(passwordPrompt[mVersion]), 0, false) != -1 ) {
|
|
mConnectState = STATE_TRY_PASSWD;
|
|
}
|
|
else if( line.find(passphrasePrompt[mVersion]) != -1 ) {
|
|
mConnectState = STATE_TRY_PASSPHRASE;
|
|
}
|
|
else if( line.find(authSuccessMsg[mVersion]) != -1 ) {
|
|
return true;
|
|
}
|
|
else if( line.find(authFailedMsg[mVersion]) != -1
|
|
&& line.find(tryAgainMsg[mVersion]) == -1 ) {
|
|
mConnectState = STATE_AUTH_FAILED;
|
|
}
|
|
else if( line.find(hostKeyMissingMsg[mVersion]) != -1 ) {
|
|
mConnectState = STATE_NEW_KEY_WAIT_CONTINUE;
|
|
}
|
|
else if( line.find(hostKeyChangedMsg[mVersion]) != -1 ) {
|
|
mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
|
|
}
|
|
else if( line.find(continuePrompt[mVersion]) != -1 ) {
|
|
//mConnectState = STATE_SEND_CONTINUE;
|
|
kdDebug(KSSHPROC) << "KSshProcess:connect(): "
|
|
"Got continue prompt where we shouldn't (STATE_WAIT_PROMPT)"
|
|
<< endl;
|
|
mError = ERR_INTERACT;
|
|
mErrorMsg =
|
|
i18n("Error encountered while talking to ssh.");
|
|
}
|
|
else if( line.find(connectionClosedMsg[mVersion]) != -1 ) {
|
|
mConnectState = STATE_FATAL;
|
|
mError = ERR_CLOSED_BY_REMOTE_HOST;
|
|
mErrorMsg = i18n("Connection closed by remote host.");
|
|
}
|
|
else if( line.find(changeHostKeyOnDiskPrompt[mVersion]) != -1 ) {
|
|
// always say yes to this. It always comes after commerical ssh
|
|
// prints a "continue to connect prompt". We assume that if the
|
|
// user choose to continue, then they also want to save the
|
|
// host key to disk.
|
|
ssh.writeLine("yes");
|
|
}
|
|
else {
|
|
// ignore line
|
|
}
|
|
break;
|
|
|
|
// STATE_TRY_PASSWD:
|
|
// If we have password send it to the ssh process, else
|
|
// set error ERR_NEED_PASSWD and return false to the caller.
|
|
// The caller then must then call KSshProcess::setPassword(TQString)
|
|
// before calling KSshProcess::connect() again.
|
|
//
|
|
// Almost exactly liek STATE_TRY_PASSPHRASE. Check there if you
|
|
// make changes here.
|
|
case STATE_TRY_PASSWD:
|
|
// We have a password prompt waiting for us to supply
|
|
// a password. Send that password to ssh. If the caller
|
|
// did not supply a password like we asked, then ask
|
|
// again.
|
|
if( !mPassword.isEmpty() ) {
|
|
// ssh.WaitSlave();
|
|
ssh.writeLine(mPassword.latin1());
|
|
|
|
// Overwrite the password so it isn't in memory.
|
|
mPassword.fill(TQChar('X'));
|
|
|
|
// Set the password to null so we will request another
|
|
// password if this one fails.
|
|
mPassword = TQString::null;
|
|
|
|
mConnectState = STATE_WAIT_PROMPT;
|
|
}
|
|
else {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect() "
|
|
"Need password from caller." << endl;
|
|
// The caller needs to supply a password before
|
|
// connecting can continue.
|
|
mError = ERR_NEED_PASSWD;
|
|
mErrorMsg = i18n("Please supply a password.");
|
|
mConnectState = STATE_TRY_PASSWD;
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
// STATE_TRY_KEY_PASSPHRASE:
|
|
// If we have passphrase send it to the ssh process, else
|
|
// set error ERR_NEED_PASSPHRASE and return false to the caller.
|
|
// The caller then must then call KSshProcess::setPassword(TQString)
|
|
// before calling KSshProcess::connect() again.
|
|
//
|
|
// Almost exactly like STATE_TRY_PASSWD. The only difference is
|
|
// the error we set if we don't have a passphrase. We duplicate
|
|
// this code to keep in the spirit of the state machine.
|
|
case STATE_TRY_PASSPHRASE:
|
|
// We have a passphrase prompt waiting for us to supply
|
|
// a passphrase. Send that passphrase to ssh. If the caller
|
|
// did not supply a passphrase like we asked, then ask
|
|
// again.
|
|
if( !mPassword.isEmpty() ) {
|
|
// ssh.WaitSlave();
|
|
ssh.writeLine(mPassword.latin1());
|
|
|
|
// Overwrite the password so it isn't in memory.
|
|
mPassword.fill(TQChar('X'));
|
|
|
|
// Set the password to null so we will request another
|
|
// password if this one fails.
|
|
mPassword = TQString::null;
|
|
|
|
mConnectState = STATE_WAIT_PROMPT;
|
|
}
|
|
else {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect() "
|
|
"Need passphrase from caller." << endl;
|
|
// The caller needs to supply a passphrase before
|
|
// connecting can continue.
|
|
mError = ERR_NEED_PASSPHRASE;
|
|
mErrorMsg = i18n("Please supply the passphrase for "
|
|
"your SSH private key.");
|
|
mConnectState = STATE_TRY_PASSPHRASE;
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
// STATE_AUTH_FAILED:
|
|
// Authentication has failed. Tell the caller by setting the
|
|
// ERR_AUTH_FAILED error and returning false. If
|
|
// auth has failed then ssh should have exited, but
|
|
// we will kill it to make sure.
|
|
case STATE_AUTH_FAILED:
|
|
mError = ERR_AUTH_FAILED;
|
|
mErrorMsg = i18n("Authentication to %1 failed").arg(mHost);
|
|
mConnectState = STATE_FATAL;
|
|
break;
|
|
|
|
// STATE_NEW_KEY_WAIT_CONTINUE:
|
|
// Grab lines from ssh until we get a continue prompt or a auth
|
|
// denied. We will get the later if StrictHostKeyChecking is set
|
|
// to yes. Go to STATE_NEW_KEY_CONTINUE if we get a continue prompt.
|
|
case STATE_NEW_KEY_WAIT_CONTINUE:
|
|
line = getLine();
|
|
if( line.isNull() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"Got null line in STATE_NEW_KEY_WAIT_CONTINUE." << endl;
|
|
mError = ERR_INTERACT;
|
|
mErrorMsg =
|
|
i18n("Error encountered while talking to ssh.");
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
else if( (line.find(authFailedMsg[mVersion]) != -1
|
|
&& line.find(tryAgainMsg[mVersion]) == -1)
|
|
|| line.find(hostKeyVerifyFailedMsg[mVersion]) != -1 ) {
|
|
mError = ERR_AUTH_FAILED_NEW_KEY;
|
|
mErrorMsg = i18n(
|
|
"The identity of the remote host '%1' could not be verified "
|
|
"because the host's key is not in the \"known hosts\" file."
|
|
).arg(mHost);
|
|
|
|
if( mKnownHostsFile.isEmpty() ) {
|
|
mErrorMsg += i18n(
|
|
" Manually, add the host's key to the \"known hosts\" "
|
|
"file or contact your administrator."
|
|
);
|
|
}
|
|
else {
|
|
mErrorMsg += i18n(
|
|
" Manually, add the host's key to %1 "
|
|
"or contact your administrator."
|
|
).arg(mKnownHostsFile);
|
|
}
|
|
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
else if( line.find(continuePrompt[mVersion]) != -1 ) {
|
|
mConnectState = STATE_NEW_KEY_CONTINUE;
|
|
}
|
|
else if( line.find(connectionClosedMsg[mVersion]) != -1 ) {
|
|
mConnectState = STATE_FATAL;
|
|
mError = ERR_CLOSED_BY_REMOTE_HOST;
|
|
mErrorMsg = i18n("Connection closed by remote host.");
|
|
}
|
|
else if( line.find(keyFingerprintMsg[mVersion]) != -1 ) {
|
|
mKeyFingerprint = keyFingerprintMsg[mVersion].cap();
|
|
kdDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint << endl;
|
|
mConnectState = STATE_NEW_KEY_WAIT_CONTINUE;
|
|
}
|
|
else {
|
|
// ignore line
|
|
}
|
|
break;
|
|
|
|
|
|
// STATE_NEW_KEY_CONTINUE:
|
|
// We got a continue prompt for the new key message. Set the error
|
|
// message to reflect this, return false and hope for caller response.
|
|
case STATE_NEW_KEY_CONTINUE:
|
|
mError = ERR_NEW_HOST_KEY;
|
|
mErrorMsg = i18n(
|
|
"The identity of the remote host '%1' could not be "
|
|
"verified. The host's key fingerprint is:\n%2\nYou should "
|
|
"verify the fingerprint with the host's administrator before "
|
|
"connecting.\n\n"
|
|
"Would you like to accept the host's key and connect anyway? "
|
|
).arg(mHost).arg(mKeyFingerprint);
|
|
mConnectState = STATE_SEND_CONTINUE;
|
|
return false;
|
|
|
|
// STATE_DIFF_KEY_WAIT_CONTINUE:
|
|
// Grab lines from ssh until we get a continue prompt or a auth
|
|
// denied. We will get the later if StrictHostKeyChecking is set
|
|
// to yes. Go to STATE_DIFF_KEY_CONTINUE if we get a continue prompt.
|
|
case STATE_DIFF_KEY_WAIT_CONTINUE:
|
|
line = getLine();
|
|
if( line.isNull() ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"Got null line in STATE_DIFF_KEY_WAIT_CONTINUE." << endl;
|
|
mError = ERR_INTERACT;
|
|
mErrorMsg =
|
|
i18n("Error encountered while talking to ssh.");
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
else if( (line.find(authFailedMsg[mVersion]) != -1
|
|
&& line.find(tryAgainMsg[mVersion]) == -1)
|
|
|| line.find(hostKeyVerifyFailedMsg[mVersion]) != -1 ) {
|
|
mError = ERR_AUTH_FAILED_DIFF_KEY;
|
|
mErrorMsg = i18n(
|
|
"WARNING: The identity of the remote host '%1' has changed!\n\n"
|
|
"Someone could be eavesdropping on your connection, or the "
|
|
"administrator may have just changed the host's key. "
|
|
"Either way, you should verify the host's key fingerprint with the host's "
|
|
"administrator. The key fingerprint is:\n%2\n"
|
|
"Add the correct host key to \"%3\" to "
|
|
"get rid of this message."
|
|
).arg(mHost).arg(mKeyFingerprint).arg(mKnownHostsFile);
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
else if( line.find(continuePrompt[mVersion]) != -1 ) {
|
|
mConnectState = STATE_DIFF_KEY_CONTINUE;
|
|
}
|
|
else if( line.find(keyFingerprintMsg[mVersion]) != -1 ) {
|
|
mKeyFingerprint = keyFingerprintMsg[mVersion].cap();
|
|
kdDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint << endl;
|
|
mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
|
|
}
|
|
else if( line.find(knownHostsFileMsg[mVersion]) != -1 ) {
|
|
mKnownHostsFile = (knownHostsFileMsg[mVersion]).cap(1);
|
|
kdDebug(KSSHPROC) << "Found known hosts file name: " << mKnownHostsFile << endl;
|
|
mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
|
|
}
|
|
else {
|
|
// ignore line
|
|
}
|
|
break;
|
|
|
|
// STATE_DIFF_KEY_CONTINUE:
|
|
// We got a continue prompt for the different key message.
|
|
// Set ERR_DIFF_HOST_KEY error
|
|
// and return false to signal need to caller action.
|
|
case STATE_DIFF_KEY_CONTINUE:
|
|
mError = ERR_DIFF_HOST_KEY;
|
|
mErrorMsg = i18n(
|
|
"WARNING: The identity of the remote host '%1' has changed!\n\n"
|
|
"Someone could be eavesdropping on your connection, or the "
|
|
"administrator may have just changed the host's key. "
|
|
"Either way, you should verify the host's key fingerprint with the host's "
|
|
"administrator before connecting. The key fingerprint is:\n%2\n\n"
|
|
"Would you like to accept the host's new key and connect anyway?"
|
|
).arg(mHost).arg(mKeyFingerprint);
|
|
mConnectState = STATE_SEND_CONTINUE;
|
|
return false;
|
|
|
|
// STATE_SEND_CONTINUE:
|
|
// We found a continue prompt. Send our answer.
|
|
case STATE_SEND_CONTINUE:
|
|
if( mAcceptHostKey ) {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"host key accepted" << endl;
|
|
ssh.writeLine("yes");
|
|
mConnectState = STATE_WAIT_PROMPT;
|
|
}
|
|
else {
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"host key rejected" << endl;
|
|
ssh.writeLine("no");
|
|
mError = ERR_HOST_KEY_REJECTED;
|
|
mErrorMsg = i18n("Host key was rejected.");
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
break;
|
|
|
|
// STATE_FATAL:
|
|
// Something bad happened that we cannot recover from.
|
|
// Kill the ssh process and set flags to show we have
|
|
// ended the connection and killed ssh.
|
|
//
|
|
// mError and mErrorMsg should be set by the immediately
|
|
// previous state.
|
|
case STATE_FATAL:
|
|
kill();
|
|
mConnected = false;
|
|
mRunning = false;
|
|
mConnectState = STATE_START;
|
|
// mError, mErroMsg set by last state
|
|
return false;
|
|
|
|
default:
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): "
|
|
"Invalid state number - " << mConnectState << endl;
|
|
mError = ERR_INVALID_STATE;
|
|
mConnectState = STATE_FATAL;
|
|
}
|
|
}
|
|
|
|
// we should never get here
|
|
kdDebug(KSSHPROC) << "KSshProcess::connect(): " <<
|
|
"After switch(). We shouldn't be here." << endl;
|
|
mError = ERR_INTERNAL;
|
|
return false;
|
|
}
|
|
|
|
void KSshProcess::disconnect() {
|
|
kill();
|
|
mConnected = false;
|
|
mRunning = false;
|
|
mConnectState = STATE_START;
|
|
}
|
|
|