delay a few things on startup, such as setting the visibility mode, which ensures...
[personal-kdebase.git] / runtime / kioslave / sftp / ksshprocess.cpp
blob33598e7670fd8861dc5eeab3a8564fbbfed41023
1 /***************************************************************************
2 ksshprocess.cpp - description
3 -------------------
4 begin : Tue Jul 31 2001
5 copyright : (C) 2001 by Lucas Fisher
6 email : ljfisher@purdue.edu
7 ***************************************************************************/
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
19 * See the KSshProcess header for examples on use.
21 * This class uses a hacked version of the PTYProcess
22 * class. This was needed because the kdelibs PTYProcess does not provide
23 * access to the pty file descriptor which we need, because ssh prints the
24 * password prompt to the pty and reads the password from the pty. I don't
25 * feel I know enough about ptys to confidently modify the orignial
26 * PTYProcess class.
28 * To start ssh we take the arguments the user gave us
29 * in the SshOptList and build the ssh command arguments based on the version
30 * of ssh we are using. This command and its arguments are passed to
31 * PTYProcess for execution. Once ssh is started we scan each line of input
32 * from stdin, stderr, and the pty for recognizable strings. The recognizable
33 * strings are taken from several string tables. Each table contains a string
34 * for each specific version of ssh we support and a string for a generic
35 * version of OpenSSH and commercial SSH incase we don't recognized the
36 * specific ssh version strings (as when a new SSH version is released after
37 * a release of KSshProcess). There are tables for ssh version strings,
38 * password prompts, new host key errors, different host key errors,
39 * messages than indicate a successful connect, authentication errors, etc.
40 * If we find user interaction is necessary, for instance to provide a
41 * password or passphrase, we return a err code to the user who can send
42 * a message to KSshProcess, using one of several methods, to correct
43 * the error.
45 * Determining when the ssh connection has successfully authenticationed has
46 * proved to be the most difficult challenge. OpenSSH does not print a message
47 * on successful authentication, thus the only way to know is to send data
48 * and wait for a return. The problem here is sometimes it can take a bit
49 * to establish the connection (for example, do to DNS lookups). This means
50 * the user may be sitting there waiting for a connection that failed.
51 * Instead, ssh is always started with the verbose flag. Then we look for
52 * a message that indicates auth succeeded. This is hazardous because
53 * debug messages are more likely to change between OpenSSH releases.
54 * Thus, we could become incompatible with new OpenSSH releases.
57 #include "ksshprocess.h"
59 #include <config-runtime.h>
61 #include <stdio.h>
62 #include <errno.h>
64 #ifdef HAVE_SYS_TIME_H
65 #include <sys/time.h>
66 #endif
68 #include <kstandarddirs.h>
69 #include <klocale.h>
70 #include <QRegExp>
73 * The following are tables of string and regexps we match
74 * against the output of ssh. An entry in each array
75 * corresponds to the version of ssh found in versionStrs[].
77 * The version strings must be ordered in the array from most
78 * specific to least specific in cases where the beginning
79 * of several version strings are the similar. For example,
80 * consider the openssh version strings. The generic "OpenSSH"
81 * must be the last of the openssh version strings in the array
82 * so that is matched last. We use these generic version strings
83 * so we can do a best effor to support unknown ssh versions.
85 QRegExp KSshProcess::versionStrs[] = {
86 QRegExp("OpenSSH_3\\.[6-9]|OpenSSH_[1-9]*[4-9]\\.[0-9]"),
87 QRegExp("OpenSSH"),
88 QRegExp("SSH Secure Shell"),
89 QRegExp("plink: Release")
92 const char * const KSshProcess::passwordPrompt[] = {
93 "password:", // OpenSSH
94 "password:", // OpenSSH
95 "password:", // SSH
96 "password:", // putty
99 const char * const KSshProcess::passphrasePrompt[] = {
100 "Enter passphrase for key",
101 "Enter passphrase for key",
102 "Passphrase for key",
103 "???"
106 const char * const KSshProcess::authSuccessMsg[] = {
107 "Authentication succeeded",
108 "ssh-userauth2 successful",
109 "Received SSH_CROSS_AUTHENTICATED packet",
110 "Started a shell/command"
113 const char* const KSshProcess::authFailedMsg[] = {
114 "Permission denied (",
115 "Permission denied (",
116 "Authentication failed.",
117 "Access denied"
120 const char* const KSshProcess::tryAgainMsg[] = {
121 "please try again",
122 "please try again",
123 "adjfhjsdhfdsjfsjdfhuefeufeuefe",
124 "???"
127 QRegExp KSshProcess::hostKeyMissingMsg[] = {
128 QRegExp("The authenticity of host|No (DSA|RSA) host key is known for"),
129 QRegExp("The authenticity of host|No (DSA|RSA) host key is known for"),
130 QRegExp("Host key not found from database"),
131 QRegExp("???")
134 const char* const KSshProcess::continuePrompt[] = {
135 "Are you sure you want to continue connecting (yes/no)?",
136 "Are you sure you want to continue connecting (yes/no)?",
137 "Are you sure you want to continue connecting (yes/no)?",
138 "???"
141 const char* const KSshProcess::hostKeyChangedMsg[] = {
142 "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!",
143 "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!",
144 "WARNING: HOST IDENTIFICATION HAS CHANGED!",
145 "???"
148 QRegExp KSshProcess::keyFingerprintMsg[] = {
149 QRegExp("..(:..){15}"),
150 QRegExp("..(:..){15}"),
151 QRegExp(".....(-.....){10}"),
152 QRegExp("???")
155 QRegExp KSshProcess::knownHostsFileMsg[] = {
156 QRegExp("Add correct host key in (.*) to get rid of this message."),
157 QRegExp("Add correct host key in (.*) to get rid of this message."),
158 QRegExp("Add correct host key to \"(.*)\""),
159 QRegExp("???")
163 // This prompt only applies to commercial ssh.
164 const char* const KSshProcess::changeHostKeyOnDiskPrompt[] = {
165 "as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf",
166 "as;jf;sajkfdslkfjas;dfjdsa;fj;dsajfdsajf",
167 "Do you want to change the host key on disk (yes/no)?",
168 "???"
171 // We need this in addition the authFailedMsg because when
172 // OpenSSH gets a changed host key it will fail to connect
173 // depending on the StrictHostKeyChecking option. Depending
174 // how this option is set, it will print "Permission denied"
175 // and quit, or print "Host key verification failed." and
176 // quit. The later if StrictHostKeyChecking is "no".
177 // The former if StrictHostKeyChecking is
178 // "yes" or explicitly set to "ask".
179 QRegExp KSshProcess::hostKeyVerifyFailedMsg[] = {
180 QRegExp("Host key verification failed\\."),
181 QRegExp("Host key verification failed\\."),
182 QRegExp("Disconnected; key exchange or algorithm? negotiation failed \\(Key exchange failed\\.\\)\\."),
183 QRegExp("???")
186 const char * const KSshProcess::connectionClosedMsg[] = {
187 "Connection closed by remote host",
188 "Connection closed by remote host",
189 "Connection closed by remote host",
190 "???"
194 void KSshProcess::SIGCHLD_handler(int)
196 while(waitpid(-1, NULL, WNOHANG) > 0) {
201 void KSshProcess::installSignalHandlers() {
202 #ifndef Q_WS_WIN
203 struct sigaction act;
204 memset(&act,0,sizeof(act));
205 act.sa_handler = SIGCHLD_handler;
206 act.sa_flags = 0
207 #ifdef SA_NOCLDSTOP
208 | SA_NOCLDSTOP
209 #endif
210 #ifdef SA_RESTART
211 | SA_RESTART
212 #endif
214 sigaction(SIGCHLD,&act,NULL);
215 #endif
218 void KSshProcess::removeSignalHandlers() {
219 #ifndef Q_WS_WIN
220 struct sigaction act;
221 memset(&act,0,sizeof(act));
222 act.sa_handler = SIG_DFL;
223 sigaction(SIGCHLD,&act,NULL);
224 #endif
227 KSshProcess::KSshProcess()
228 : mVersion(UNKNOWN_VER), mConnected(false),
229 mRunning(false), mConnectState(0) {
230 #ifndef Q_WS_WIN
231 mSshPath = KStandardDirs::findExe(QString::fromLatin1("ssh"));
232 #else
233 mSshPath = KStandardDirs::findExe(QString::fromLatin1("plink"));
234 #endif
235 kDebug(KSSHPROC) << "KSshProcess::KSshProcess(): ssh path [" <<
236 mSshPath << "]" << endl;
238 installSignalHandlers();
241 KSshProcess::KSshProcess(QString pathToSsh)
242 : mSshPath(pathToSsh), mVersion(UNKNOWN_VER), mConnected(false),
243 mRunning(false), mConnectState(0) {
244 installSignalHandlers();
247 KSshProcess::~KSshProcess(){
248 disconnect();
249 removeSignalHandlers();
250 while(waitpid(-1, NULL, WNOHANG) > 0) {
255 bool KSshProcess::setSshPath(QString pathToSsh) {
256 mSshPath = pathToSsh;
257 version();
258 if( mVersion == UNKNOWN_VER )
259 return false;
261 return true;
264 KSshProcess::SshVersion KSshProcess::version() {
265 QString cmd;
266 cmd = mSshPath+" -V 2>&1";
268 // Get version string from ssh client.
269 FILE *p;
270 if( (p = popen(cmd.toLatin1(), "r")) == NULL ) {
271 kDebug(KSSHPROC) << "KSshProcess::version(): "
272 "failed to start ssh: " << strerror(errno) << endl;
273 return UNKNOWN_VER;
276 // Determine of the version from the version string.
277 size_t len;
278 char buf[128];
279 if( (len = fread(buf, sizeof(char), sizeof(buf)-1, p)) == 0 ) {
280 kDebug(KSSHPROC) << "KSshProcess::version(): "
281 "Read of ssh version string failed " <<
282 strerror(ferror(p)) << endl;
283 return UNKNOWN_VER;
285 if( pclose(p) == -1 ) {
286 kError(KSSHPROC) << "KSshProcess::version(): pclose failed." << endl;
288 buf[len] = '\0';
289 QString ver;
290 ver = buf;
291 kDebug(KSSHPROC) << "KSshProcess::version(): "
292 "got version string [" << ver << "]" << endl;
294 mVersion = UNKNOWN_VER;
295 for(int i = 0; i < SSH_VER_MAX; i++) {
296 if( ver.indexOf(versionStrs[i]) != -1 ) {
297 mVersion = (SshVersion)i;
298 break;
302 kDebug(KSSHPROC) << "KSshPRocess::version(): version number = "
303 << mVersion << endl;
305 if( mVersion == UNKNOWN_VER ) {
306 kDebug(KSSHPROC) << "KSshProcess::version(): "
307 "Sorry, I don't know about this version of ssh" << endl;
308 mError = ERR_UNKNOWN_VERSION;
309 return UNKNOWN_VER;
312 return mVersion;
315 QString KSshProcess::versionStr() {
316 if( mVersion == UNKNOWN_VER ) {
317 version();
318 if( mVersion == UNKNOWN_VER )
319 return QString();
322 return QString::fromLatin1(versionStrs[mVersion]);
326 bool KSshProcess::setOptions(const SshOptList& opts) {
327 kDebug(KSSHPROC) << "KSshProcess::setOptions()";
329 if( mVersion == UNKNOWN_VER ) {
330 // we don't know the ssh version yet, so find out
331 version();
332 if( mVersion == UNKNOWN_VER ) {
333 return false;
337 mArgs.clear();
338 SshOptListConstIterator it;
339 QString cmd, subsystem;
340 mPassword = mUsername = mHost = QString();
341 QByteArray tmp;
342 for(it = opts.begin(); it != opts.end(); ++it) {
343 //kDebug(KSSHPROC) << "opt.opt = " << (*it).opt;
344 //kDebug(KSSHPROC) << "opt.str = " << (*it).str;
345 //kDebug(KSSHPROC) << "opt.num = " << (*it).num;
346 switch( (*it).opt ) {
347 case SSH_VERBOSE:
348 mArgs.append("-v");
349 break;
351 case SSH_SUBSYSTEM:
352 subsystem = (*it).str;
353 break;
355 case SSH_PORT:
356 if( mVersion == PLINK ) {
357 mArgs.append("-P");
358 } else {
359 mArgs.append("-p");
361 tmp.setNum((*it).num);
362 mArgs.append(tmp);
363 mPort = (*it).num;
364 break;
366 case SSH_HOST:
367 mHost = (*it).str;
368 break;
370 case SSH_USERNAME:
371 mArgs.append("-l");
372 mArgs.append((*it).str.toLatin1());
373 mUsername = (*it).str;
374 break;
376 case SSH_PASSWD:
377 mPassword = (*it).str;
378 break;
380 case SSH_PROTOCOL:
381 if( mVersion <= OPENSSH ) {
382 tmp = "Protocol=";
383 tmp += QString::number((*it).num).toLatin1();
384 mArgs.append("-o");
385 mArgs.append(tmp);
387 else if( mVersion <= SSH || mVersion == PLINK ) {
388 if( (*it).num == 1 ) {
389 mArgs.append("-1");
391 // else uses version 2 by default
393 break;
395 case SSH_FORWARDX11:
396 if( mVersion == PLINK) {
397 mArgs.append((*it).boolean ? "-X" : "-x");
398 } else {
399 tmp = "ForwardX11=";
400 tmp += (*it).boolean ? "yes" : "no";
401 mArgs.append("-o");
402 mArgs.append(tmp);
404 break;
406 case SSH_FORWARDAGENT:
407 if( mVersion == PLINK) {
408 mArgs.append((*it).boolean ? "-A" : "-a");
409 } else {
410 tmp = "ForwardAgent=";
411 tmp += (*it).boolean ? "yes" : "no";
412 mArgs.append("-o");
413 mArgs.append(tmp);
415 break;
417 case SSH_ESCAPE_CHAR:
418 if( mVersion != PLINK ) {
419 if( (*it).num == -1 )
420 tmp = "none";
421 else
422 tmp = QByteArray( (char)((*it).num), '\0' );
423 mArgs.append("-e");
424 mArgs.append(tmp);
426 break;
428 case SSH_OPTION:
429 // don't allow NumberOfPasswordPrompts or StrictHostKeyChecking
430 // since KSshProcess depends on specific setting of these for
431 // preforming authentication correctly.
432 if( mVersion != PLINK ) {
433 tmp = (*it).str.toLatin1();
434 if( tmp.contains("NumberOfPasswordPrompts") ||
435 tmp.contains("StrictHostKeyChecking") ) {
436 mError = ERR_INVALID_OPT;
437 return false;
439 else {
440 mArgs.append("-o");
441 mArgs.append(tmp);
444 break;
446 case SSH_COMMAND:
447 cmd = (*it).str;
448 break;
450 default:
451 kDebug(KSSHPROC) << "KSshProcess::setOptions(): "
452 "unrecognized ssh opt " << (*it).opt << endl;
456 if( !subsystem.isEmpty() && !cmd.isEmpty() ) {
457 kDebug(KSSHPROC) << "KSshProcess::setOptions(): "
458 "cannot use a subsystem and command at the same time" << endl;
459 mError = ERR_CMD_SUBSYS_CONFLICT;
460 mErrorMsg = i18n("Cannot specify a subsystem and command at the same time.");
461 return false;
464 // These options govern the behavior of ssh and
465 // cannot be defined by the user
466 //mArgs.append("-o");
467 //mArgs.append("StrictHostKeyChecking=ask");
468 mArgs.append("-v"); // So we get a message that the
469 // connection was successful
470 if( mVersion <= OPENSSH ) {
471 // nothing
473 else if( mVersion <= SSH ) {
474 mArgs.append("-o"); // So we can check if the connection was successful
475 mArgs.append("AuthenticationSuccessMsg=yes");
478 if( mHost.isEmpty() ) {
479 kDebug(KSSHPROC) << "KSshProcess::setOptions(): "
480 "a host name must be supplied" << endl;
481 return false;
483 else {
484 mArgs.append(mHost.toLatin1());
487 if( !subsystem.isEmpty() ) {
488 mArgs.append("-s");
489 mArgs.append(subsystem.toLatin1());
492 if( !cmd.isEmpty() ) {
493 mArgs.append(cmd.toLatin1());
496 return true;
499 void KSshProcess::printArgs() {
500 QList<QByteArray>::Iterator it;
501 for( it = mArgs.begin(); it != mArgs.end(); ++it) {
502 kDebug(KSSHPROC) << "arg: " << *it;
507 int KSshProcess::error(QString& msg) {
508 kDebug(KSSHPROC) << "KSshProcess::error()";
509 kDebug() << mErrorMsg;
510 msg = mErrorMsg;
511 return mError;
514 void KSshProcess::kill(int signal) {
515 int pid = ssh.pid();
517 kDebug(KSSHPROC) << "KSshProcess::kill(signal:" << signal
518 << "): ssh pid is " << pid << endl;
519 kDebug(KSSHPROC) << "KSshPRocess::kill(): we are "
520 << (mConnected ? "" : "not ") << "connected" << endl;
521 kDebug(KSSHPROC) << "KSshProcess::kill(): we are "
522 << (mRunning ? "" : "not ") << "running a ssh process" << endl;
524 if( mRunning && pid > 1 ) {
525 // Kill the child process...
526 if ( ::kill(pid, signal) == 0 ) {
527 // clean up if we tried to kill the process
528 if( signal == SIGTERM || signal == SIGKILL ) {
529 while(waitpid(-1, NULL, WNOHANG) > 0) {
532 mConnected = false;
533 mRunning = false;
536 else
537 kDebug(KSSHPROC) << "KSshProcess::kill(): kill failed";
539 else
540 kDebug(KSSHPROC) << "KSshProcess::kill(): "
541 "Refusing to kill ssh process" << endl;
547 * Try to open an ssh connection.
548 * SSH prints certain messages to certain file descriptiors:
549 * passwordPrompt - pty
550 * passphrasePrompt - pty
551 * authSuccessMsg - stderr (OpenSSH),
552 * authFailedMsg - stderr
553 * hostKeyMissing - stderr
554 * hostKeyChanged - stderr
555 * continuePrompt - stderr
557 * We will use a select to wait for a line on each descriptor. Then get
558 * each line that available and take action based on it. The type
559 * of messages we are looking for and the action we take on each
560 * message are:
561 * passwordPrompt - Return false, set error to ERR_NEED_PASSWD.
562 * On the next call to connect() we expect a password
563 * to be available.
565 * passpharsePrompt - Return false, set error to ERR_NEED_PASSPHRASE.
566 * On the next call to connect() we expect a
567 * passphrase to be available.
569 * authSuccessMsg - Return true, as we have successfully established a
570 * ssh connection.
572 * authFailedMsg - Return false, set error to ERR_AUTH_FAILED. We
573 * were unable to authenticate the connection given
574 * the available authentication information.
576 * hostKeyMissing - Return false, set error to ERR_NEW_HOST_KEY. Caller
577 * must call KSshProcess.acceptHostKey(bool) to accept
578 * or reject the key before calling connect() again.
580 * hostKeyChanged - Return false, set error to ERR_DIFF_HOST_KEY. Caller
581 * must call KSshProcess.acceptHostKey(bool) to accept
582 * or reject the key before calling connect() again.
584 * continuePrompt - Send 'yes' or 'no' to accept or reject a key,
585 * respectively.
590 void KSshProcess::acceptHostKey(bool accept) {
591 kDebug(KSSHPROC) << "KSshProcess::acceptHostKey(accept:"
592 << accept << ")" << endl;
593 mAcceptHostKey = accept;
596 void KSshProcess::setPassword(QString password) {
597 kDebug(KSSHPROC) << "KSshProcess::setPassword(password:xxxxxxxx)";
598 mPassword = password;
601 QString KSshProcess::getLine() {
602 #ifdef Q_WS_WIN
603 return ssh.readLineFromPty(false);
604 #else
605 static QStringList buffer;
606 QString line = QString();
607 QByteArray ptyLine, errLine;
609 if( buffer.empty() ) {
610 // PtyProcess buffers lines. First check that there
611 // isn't something on the PtyProces buffer or that there
612 // is not data ready to be read from the pty or stderr.
613 ptyLine = ssh.readLineFromPty(false);
614 errLine = ssh.readLineFromStderr(false);
616 // If PtyProcess did have something for us, get it and
617 // place it in our line buffer.
618 if( ! ptyLine.isEmpty() ) {
619 buffer.prepend(QString(ptyLine));
622 if( ! errLine.isEmpty() ) {
623 buffer.prepend(QString(errLine));
626 // If we still don't have anything in our buffer so there must
627 // not be anything on the pty or stderr. Setup a select()
628 // to wait for some data from SSH.
629 if( buffer.empty() ) {
630 //kDebug(KSSHPROC) << "KSshProcess::getLine(): " <<
631 // "Line buffer empty, calling select() to wait for data." << endl;
632 int errfd = ssh.stderrFd();
633 int ptyfd = ssh.fd();
634 fd_set rfds;
635 fd_set efds;
636 struct timeval tv;
638 // find max file descriptor
639 int maxfd = ptyfd > errfd ? ptyfd : errfd;
641 FD_ZERO(&rfds);
642 FD_SET(ptyfd, &rfds); // Add pty file descriptor
643 FD_SET(errfd, &rfds); // Add std error file descriptor
645 FD_ZERO(&efds);
646 FD_SET(ptyfd, &efds);
647 FD_SET(errfd, &efds);
649 tv.tv_sec = 60; tv.tv_usec = 0; // 60 second timeout
651 // Wait for a message from ssh on stderr or the pty.
652 int ret = -1;
654 ret = ::select(maxfd+1, &rfds, NULL, &efds, &tv);
655 while( ret == -1 && errno == EINTR );
657 // Handle any errors from select
658 if( ret == 0 ) {
659 kDebug(KSSHPROC) << "KSshProcess::connect(): " <<
660 "timed out waiting for a response" << endl;
661 mError = ERR_TIMED_OUT;
662 return QString();
664 else if( ret == -1 ) {
665 kDebug(KSSHPROC) << "KSshProcess::connect(): "
666 << "select error: " << strerror(errno) << endl;
667 mError = ERR_INTERNAL;
668 return QString();
671 // We are not respecting any type of order in which the
672 // lines were received. Who knows whether pty or stderr
673 // had data on it first.
674 if( FD_ISSET(ptyfd, &rfds) ) {
675 ptyLine = ssh.readLineFromPty(false);
676 buffer.prepend(QString(ptyLine));
677 //kDebug(KSSHPROC) << "KSshProcess::getLine(): "
678 // "line from pty -" << ptyLine << endl;
681 if( FD_ISSET(errfd, &rfds) ) {
682 errLine = ssh.readLineFromStderr(false);
683 buffer.prepend(QString(errLine));
684 //kDebug(KSSHPROC) << "KSshProcess::getLine(): "
685 // "line from err -" << errLine << endl;
688 if( FD_ISSET(ptyfd, &efds) ) {
689 kDebug(KSSHPROC) << "KSshProcess::getLine(): "
690 "Exception on pty file descriptor." << endl;
693 if( FD_ISSET(errfd, &efds) ) {
694 kDebug(KSSHPROC) << "KSshProcess::getLine(): "
695 "Exception on std err file descriptor." << endl;
701 // We should have something in our buffer now.
702 // Return the last line.
703 //it = buffer.end();
704 //line = *it;
705 //buffer.remove(it);
707 line = buffer.last();
708 buffer.pop_back();
710 if( line.isNull() && buffer.count() > 0 ) {
711 line = buffer.last();
712 buffer.pop_back();
715 // kDebug(KSSHPROC) << "KSshProcess::getLine(): " <<
716 // buffer.count() << " lines in buffer" << endl;
717 kDebug(KSSHPROC) << "KSshProcess::getLine(): "
718 "ssh: " << line << endl;
721 return line;
722 #endif
725 // All the different states we could go through while trying to connect.
726 enum sshConnectState {
727 STATE_START, STATE_TRY_PASSWD, STATE_WAIT_PROMPT, STATE_NEW_KEY_CONTINUE,
728 STATE_DIFF_KEY_CONTINUE, STATE_FATAL, STATE_WAIT_CONTINUE_PROMPT,
729 STATE_SEND_CONTINUE, STATE_AUTH_FAILED, STATE_NEW_KEY_WAIT_CONTINUE,
730 STATE_DIFF_KEY_WAIT_CONTINUE, STATE_TRY_PASSPHRASE
733 // Print the state as a string. Good for debugging
734 const char* stateStr(int state) {
735 switch(state) {
736 case STATE_START:
737 return "STATE_START";
738 case STATE_TRY_PASSWD:
739 return "STATE_TRY_PASSWD";
740 case STATE_WAIT_PROMPT:
741 return "STATE_WAIT_PROMPT";
742 case STATE_NEW_KEY_CONTINUE:
743 return "STATE_NEW_KEY_CONTINUE";
744 case STATE_DIFF_KEY_CONTINUE:
745 return "STATE_DIFF_KEY_CONTINUE";
746 case STATE_FATAL:
747 return "STATE_FATAL";
748 case STATE_WAIT_CONTINUE_PROMPT:
749 return "STATE_WAIT_CONTINUE_PROMPT";
750 case STATE_SEND_CONTINUE:
751 return "STATE_SEND_CONTINE";
752 case STATE_AUTH_FAILED:
753 return "STATE_AUTH_FAILED";
754 case STATE_NEW_KEY_WAIT_CONTINUE:
755 return "STATE_NEW_KEY_WAIT_CONTINUE";
756 case STATE_DIFF_KEY_WAIT_CONTINUE:
757 return "STATE_DIFF_KEY_WAIT_CONTINUE";
758 case STATE_TRY_PASSPHRASE:
759 return "STATE_TRY_PASSPHRASE";
761 return "UNKNOWN";
764 bool KSshProcess::connect() {
765 if( mVersion == UNKNOWN_VER ) {
766 // we don't know the ssh version yet, so find out
767 version();
768 if( mVersion == UNKNOWN_VER ) {
769 return false;
773 // We'll put a limit on the number of state transitions
774 // to ensure we don't go out of control.
775 int transitionLimit = 500;
777 while(--transitionLimit) {
778 kDebug(KSSHPROC) << "KSshProcess::connect(): "
779 << "Connect state " << stateStr(mConnectState) << endl;
781 QString line; // a line from ssh
782 QString msgBuf; // buffer for important messages from ssh
783 // which are to be returned to the user
785 switch(mConnectState) {
786 // STATE_START:
787 // Executes the ssh binary with the options provided. If no options
788 // have been specified, sets error and returns false. Continue to
789 // state 1 if execution is successful, otherwise set error and
790 // return false.
791 case STATE_START:
792 // reset some key values to safe values
793 mAcceptHostKey = false;
794 mKeyFingerprint.clear();
795 mKnownHostsFile.clear();
797 if( mArgs.isEmpty() ) {
798 kDebug(KSSHPROC) << "KSshProcess::connect(): ssh options "
799 "need to be set first using setArgs()" << endl;
800 mError = ERR_NO_OPTIONS;
801 mErrorMsg = i18n("No options provided for ssh execution.");
802 return false;
805 if( ssh.exec(mSshPath.toLatin1(), mArgs) ) {
806 kDebug(KSSHPROC) <<
807 "KSshProcess::connect(): ssh exec failed" << endl;
808 mError = ERR_CANNOT_LAUNCH;
809 mErrorMsg = i18n("Failed to execute ssh process.");
810 return false;
813 kDebug(KSSHPROC) << "KSshPRocess::connect(): ssh pid = " << ssh.pid();
815 // set flag to indicate what have started a ssh process
816 mRunning = true;
817 mConnectState = STATE_WAIT_PROMPT;
818 break;
820 // STATE_WAIT_PROMPT:
821 // Get a line of input from the ssh process. Check the contents
822 // of the line to determine the next state. Ignore the line
823 // if we don't recognize its contents. If the line contains
824 // the continue prompt, we have an error since we should never
825 // get that line in this state. Set ERR_INVALID_STATE error
826 // and return false.
827 case STATE_WAIT_PROMPT:
828 line = getLine();
829 if( line.isNull() ) {
830 kDebug(KSSHPROC) << "KSshProcess::connect(): "
831 "Got null line in STATE_WAIT_PROMPT." << endl;
832 mError = ERR_INTERACT;
833 mErrorMsg =
834 i18n("Error encountered while talking to ssh.");
835 mConnectState = STATE_FATAL;
837 else if( line.indexOf(QString::fromLatin1(passwordPrompt[mVersion]), 0, Qt::CaseInsensitive) != -1 ) {
838 mConnectState = STATE_TRY_PASSWD;
840 else if( line.indexOf(passphrasePrompt[mVersion]) != -1 ) {
841 mConnectState = STATE_TRY_PASSPHRASE;
843 else if( line.indexOf(authSuccessMsg[mVersion]) != -1 ) {
844 return true;
846 else if( (line.indexOf(authFailedMsg[mVersion]) != -1)
847 && (line.indexOf(tryAgainMsg[mVersion]) == -1) ) {
848 mConnectState = STATE_AUTH_FAILED;
850 else if( line.indexOf(hostKeyMissingMsg[mVersion]) != -1 ) {
851 mConnectState = STATE_NEW_KEY_WAIT_CONTINUE;
853 else if( line.indexOf(hostKeyChangedMsg[mVersion]) != -1 ) {
854 mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
856 else if( line.indexOf(continuePrompt[mVersion]) != -1 ) {
857 //mConnectState = STATE_SEND_CONTINUE;
858 kDebug(KSSHPROC) << "KSshProcess:connect(): "
859 "Got continue prompt where we shouldn't (STATE_WAIT_PROMPT)"
860 << endl;
861 mError = ERR_INTERACT;
862 mErrorMsg =
863 i18n("Error encountered while talking to ssh.");
865 else if( line.indexOf(connectionClosedMsg[mVersion]) != -1 ) {
866 mConnectState = STATE_FATAL;
867 mError = ERR_CLOSED_BY_REMOTE_HOST;
868 mErrorMsg = i18n("Connection closed by remote host.");
870 else if( line.indexOf(changeHostKeyOnDiskPrompt[mVersion]) != -1 ) {
871 // always say yes to this. It always comes after commercial ssh
872 // prints a "continue to connect prompt". We assume that if the
873 // user choose to continue, then they also want to save the
874 // host key to disk.
875 ssh.writeLine("yes");
877 else {
878 // ignore line
880 break;
882 // STATE_TRY_PASSWD:
883 // If we have password send it to the ssh process, else
884 // set error ERR_NEED_PASSWD and return false to the caller.
885 // The caller then must then call KSshProcess::setPassword(QString)
886 // before calling KSshProcess::connect() again.
888 // Almost exactly liek STATE_TRY_PASSPHRASE. Check there if you
889 // make changes here.
890 case STATE_TRY_PASSWD:
891 // We have a password prompt waiting for us to supply
892 // a password. Send that password to ssh. If the caller
893 // did not supply a password like we asked, then ask
894 // again.
895 if( !mPassword.isEmpty() ) {
896 // ssh.WaitSlave();
897 ssh.writeLine(mPassword.toLatin1());
899 // Overwrite the password so it isn't in memory.
900 mPassword.fill(QChar('X'));
902 // Set the password to null so we will request another
903 // password if this one fails.
904 mPassword.clear();
906 mConnectState = STATE_WAIT_PROMPT;
908 else {
909 kDebug(KSSHPROC) << "KSshProcess::connect() "
910 "Need password from caller." << endl;
911 // The caller needs to supply a password before
912 // connecting can continue.
913 mError = ERR_NEED_PASSWD;
914 mErrorMsg = i18n("Please supply a password.");
915 mConnectState = STATE_TRY_PASSWD;
916 return false;
918 break;
920 // STATE_TRY_KEY_PASSPHRASE:
921 // If we have passphrase send it to the ssh process, else
922 // set error ERR_NEED_PASSPHRASE and return false to the caller.
923 // The caller then must then call KSshProcess::setPassword(QString)
924 // before calling KSshProcess::connect() again.
926 // Almost exactly like STATE_TRY_PASSWD. The only difference is
927 // the error we set if we don't have a passphrase. We duplicate
928 // this code to keep in the spirit of the state machine.
929 case STATE_TRY_PASSPHRASE:
930 // We have a passphrase prompt waiting for us to supply
931 // a passphrase. Send that passphrase to ssh. If the caller
932 // did not supply a passphrase like we asked, then ask
933 // again.
934 if( !mPassword.isEmpty() ) {
935 // ssh.WaitSlave();
936 ssh.writeLine(mPassword.toLatin1());
938 // Overwrite the password so it isn't in memory.
939 mPassword.fill(QChar('X'));
941 // Set the password to null so we will request another
942 // password if this one fails.
943 mPassword.clear();
945 mConnectState = STATE_WAIT_PROMPT;
947 else {
948 kDebug(KSSHPROC) << "KSshProcess::connect() "
949 "Need passphrase from caller." << endl;
950 // The caller needs to supply a passphrase before
951 // connecting can continue.
952 mError = ERR_NEED_PASSPHRASE;
953 mErrorMsg = i18n("Please supply the passphrase for "
954 "your SSH private key.");
955 mConnectState = STATE_TRY_PASSPHRASE;
956 return false;
958 break;
960 // STATE_AUTH_FAILED:
961 // Authentication has failed. Tell the caller by setting the
962 // ERR_AUTH_FAILED error and returning false. If
963 // auth has failed then ssh should have exited, but
964 // we will kill it to make sure.
965 case STATE_AUTH_FAILED:
966 mError = ERR_AUTH_FAILED;
967 mErrorMsg = i18n("Authentication to %1 failed", mHost);
968 mConnectState = STATE_FATAL;
969 break;
971 // STATE_NEW_KEY_WAIT_CONTINUE:
972 // Grab lines from ssh until we get a continue prompt or a auth
973 // denied. We will get the later if StrictHostKeyChecking is set
974 // to yes. Go to STATE_NEW_KEY_CONTINUE if we get a continue prompt.
975 case STATE_NEW_KEY_WAIT_CONTINUE:
976 line = getLine();
977 if( line.isNull() ) {
978 kDebug(KSSHPROC) << "KSshProcess::connect(): "
979 "Got null line in STATE_NEW_KEY_WAIT_CONTINUE." << endl;
980 mError = ERR_INTERACT;
981 mErrorMsg =
982 i18n("Error encountered while talking to ssh.");
983 mConnectState = STATE_FATAL;
985 else if( ((line.indexOf(authFailedMsg[mVersion]) != -1)
986 && (line.indexOf(tryAgainMsg[mVersion]) == -1))
987 || (line.indexOf(hostKeyVerifyFailedMsg[mVersion]) != -1) ) {
988 mError = ERR_AUTH_FAILED_NEW_KEY;
989 mErrorMsg = i18n(
990 "The identity of the remote host '%1' could not be verified "
991 "because the host's key is not in the \"known hosts\" file."
992 , mHost);
994 if( mKnownHostsFile.isEmpty() ) {
995 mErrorMsg += i18n(
996 " Manually, add the host's key to the \"known hosts\" "
997 "file or contact your administrator."
1000 else {
1001 mErrorMsg += i18n(
1002 " Manually, add the host's key to %1 "
1003 "or contact your administrator."
1004 , mKnownHostsFile);
1007 mConnectState = STATE_FATAL;
1009 else if( line.indexOf(continuePrompt[mVersion]) != -1 ) {
1010 mConnectState = STATE_NEW_KEY_CONTINUE;
1012 else if( line.indexOf(connectionClosedMsg[mVersion]) != -1 ) {
1013 mConnectState = STATE_FATAL;
1014 mError = ERR_CLOSED_BY_REMOTE_HOST;
1015 mErrorMsg = i18n("Connection closed by remote host.");
1017 else if( line.indexOf(keyFingerprintMsg[mVersion]) != -1 ) {
1018 mKeyFingerprint = keyFingerprintMsg[mVersion].cap();
1019 kDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint;
1020 mConnectState = STATE_NEW_KEY_WAIT_CONTINUE;
1022 else {
1023 // ignore line
1025 break;
1028 // STATE_NEW_KEY_CONTINUE:
1029 // We got a continue prompt for the new key message. Set the error
1030 // message to reflect this, return false and hope for caller response.
1031 case STATE_NEW_KEY_CONTINUE:
1032 mError = ERR_NEW_HOST_KEY;
1033 mErrorMsg = i18n(
1034 "The identity of the remote host '%1' could not be "
1035 "verified. The host's key fingerprint is:\n%2\nYou should "
1036 "verify the fingerprint with the host's administrator before "
1037 "connecting.\n\n"
1038 "Would you like to accept the host's key and connect anyway? "
1039 , mHost, mKeyFingerprint);
1040 mConnectState = STATE_SEND_CONTINUE;
1041 return false;
1043 // STATE_DIFF_KEY_WAIT_CONTINUE:
1044 // Grab lines from ssh until we get a continue prompt or a auth
1045 // denied. We will get the later if StrictHostKeyChecking is set
1046 // to yes. Go to STATE_DIFF_KEY_CONTINUE if we get a continue prompt.
1047 case STATE_DIFF_KEY_WAIT_CONTINUE:
1048 line = getLine();
1049 if( line.isNull() ) {
1050 kDebug(KSSHPROC) << "KSshProcess::connect(): "
1051 "Got null line in STATE_DIFF_KEY_WAIT_CONTINUE." << endl;
1052 mError = ERR_INTERACT;
1053 mErrorMsg =
1054 i18n("Error encountered while talking to ssh.");
1055 mConnectState = STATE_FATAL;
1057 else if( ((line.indexOf(authFailedMsg[mVersion]) != -1)
1058 && (line.indexOf(tryAgainMsg[mVersion]) == -1))
1059 || (line.indexOf(hostKeyVerifyFailedMsg[mVersion]) != -1) ) {
1060 mError = ERR_AUTH_FAILED_DIFF_KEY;
1061 mErrorMsg = i18n(
1062 "WARNING: The identity of the remote host '%1' has changed!\n\n"
1063 "Someone could be eavesdropping on your connection, or the "
1064 "administrator may have just changed the host's key. "
1065 "Either way, you should verify the host's key fingerprint with the host's "
1066 "administrator. The key fingerprint is:\n%2\n"
1067 "Add the correct host key to \"%3\" to "
1068 "get rid of this message."
1069 , mHost, mKeyFingerprint, mKnownHostsFile);
1070 mConnectState = STATE_FATAL;
1072 else if( line.indexOf(continuePrompt[mVersion]) != -1 ) {
1073 mConnectState = STATE_DIFF_KEY_CONTINUE;
1075 else if( line.indexOf(keyFingerprintMsg[mVersion]) != -1 ) {
1076 mKeyFingerprint = keyFingerprintMsg[mVersion].cap();
1077 kDebug(KSSHPROC) << "Found key fingerprint: " << mKeyFingerprint;
1078 mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
1080 else if( line.indexOf(knownHostsFileMsg[mVersion]) != -1 ) {
1081 mKnownHostsFile = (knownHostsFileMsg[mVersion]).cap(1);
1082 kDebug(KSSHPROC) << "Found known hosts file name: " << mKnownHostsFile;
1083 mConnectState = STATE_DIFF_KEY_WAIT_CONTINUE;
1085 else {
1086 // ignore line
1088 break;
1090 // STATE_DIFF_KEY_CONTINUE:
1091 // We got a continue prompt for the different key message.
1092 // Set ERR_DIFF_HOST_KEY error
1093 // and return false to signal need to caller action.
1094 case STATE_DIFF_KEY_CONTINUE:
1095 mError = ERR_DIFF_HOST_KEY;
1096 mErrorMsg = i18n(
1097 "WARNING: The identity of the remote host '%1' has changed!\n\n"
1098 "Someone could be eavesdropping on your connection, or the "
1099 "administrator may have just changed the host's key. "
1100 "Either way, you should verify the host's key fingerprint with the host's "
1101 "administrator before connecting. The key fingerprint is:\n%2\n\n"
1102 "Would you like to accept the host's new key and connect anyway?"
1103 , mHost, mKeyFingerprint);
1104 mConnectState = STATE_SEND_CONTINUE;
1105 return false;
1107 // STATE_SEND_CONTINUE:
1108 // We found a continue prompt. Send our answer.
1109 case STATE_SEND_CONTINUE:
1110 if( mAcceptHostKey ) {
1111 kDebug(KSSHPROC) << "KSshProcess::connect(): "
1112 "host key accepted" << endl;
1113 ssh.writeLine("yes");
1114 mConnectState = STATE_WAIT_PROMPT;
1116 else {
1117 kDebug(KSSHPROC) << "KSshProcess::connect(): "
1118 "host key rejected" << endl;
1119 ssh.writeLine("no");
1120 mError = ERR_HOST_KEY_REJECTED;
1121 mErrorMsg = i18n("Host key was rejected.");
1122 mConnectState = STATE_FATAL;
1124 break;
1126 // STATE_FATAL:
1127 // Something bad happened that we cannot recover from.
1128 // Kill the ssh process and set flags to show we have
1129 // ended the connection and killed ssh.
1131 // mError and mErrorMsg should be set by the immediately
1132 // previous state.
1133 case STATE_FATAL:
1134 kill();
1135 mConnected = false;
1136 mRunning = false;
1137 mConnectState = STATE_START;
1138 // mError, mErroMsg set by last state
1139 return false;
1141 default:
1142 kDebug(KSSHPROC) << "KSshProcess::connect(): "
1143 "Invalid state number - " << mConnectState << endl;
1144 mError = ERR_INVALID_STATE;
1145 mConnectState = STATE_FATAL;
1149 // we should never get here
1150 kDebug(KSSHPROC) << "KSshProcess::connect(): " <<
1151 "After switch(). We shouldn't be here." << endl;
1152 mError = ERR_INTERNAL;
1153 return false;
1156 void KSshProcess::disconnect() {
1157 kill();
1158 mConnected = false;
1159 mRunning = false;
1160 mConnectState = STATE_START;