使用twisted.conch作为客户端通过ssh接收扩展数据

3 投票
1 回答
1701 浏览
提问于 2025-04-17 05:14

我现在正在通过一种比较直接的方式学习ssh,就是不停地尝试,直到我搞明白为止。经过一些反复试验,我已经成功发送了一个“pty-req”请求,然后是一个“shell”请求,这样我就能看到登录前的提示,发送命令并接收标准输出(stdout),但我不太确定怎么告诉SSH服务我想接收错误输出(stderr)和状态消息。目前查看其他SSH的实现(比如paramiko和Net::SSH)对我帮助不大。

不过,翻阅了一些关于SSH的标准文档(RFC),我觉得其中列出的一些请求可能正是我需要的:https://www.rfc-editor.org/rfc/rfc4250#section-4.9.3

#!/usr/bin/env python


from twisted.conch.ssh import transport
from twisted.conch.ssh import userauth
from twisted.conch.ssh import connection
from twisted.conch.ssh import common
from twisted.conch.ssh.common import NS
from twisted.conch.ssh import keys
from twisted.conch.ssh import channel
from twisted.conch.ssh import session
from twisted.internet import defer

from twisted.internet import defer, protocol, reactor
from twisted.python import log
import struct, sys, getpass, os
log.startLogging(sys.stdout)


USER = 'dward'  
HOST = '192.168.0.19' # pristine.local
PASSWD = "password"
PRIVATE_KEY = "~/id_rsa"

class SimpleTransport(transport.SSHClientTransport):
    def verifyHostKey(self, hostKey, fingerprint):
        print 'host key fingerprint: %s' % fingerprint
        return defer.succeed(1) 

    def connectionSecure(self):
        self.requestService(
            SimpleUserAuth(USER,
                SimpleConnection()))

class SimpleUserAuth(userauth.SSHUserAuthClient):
    def getPassword(self):
        return defer.succeed(PASSWD)

    def getGenericAnswers(self, name, instruction, questions):
        print name
        print instruction
        answers = []
        for prompt, echo in questions:
            if echo:
                answer = raw_input(prompt)
            else:
                answer = getpass.getpass(prompt)
            answers.append(answer)
        return defer.succeed(answers)
            
    def getPublicKey(self):
        path = os.path.expanduser(PRIVATE_KEY) 
        # this works with rsa too
        # just change the name here and in getPrivateKey
        if not os.path.exists(path) or self.lastPublicKey:
            # the file doesn't exist, or we've tried a public key
            return
        return keys.Key.fromFile(filename=path+'.pub').blob()

    def getPrivateKey(self):
        path = os.path.expanduser(PRIVATE_KEY)
        return defer.succeed(keys.Key.fromFile(path).keyObject)
        
    
    
class SimpleConnection(connection.SSHConnection):
    def serviceStarted(self):
        self.openChannel(SmartChannel(2**16, 2**15, self))        




class SmartChannel(channel.SSHChannel):
    name = "session"
    
    
    def getResponse(self, timeout = 10):
        self.onData = defer.Deferred()
        self.timeout = reactor.callLater( timeout, self.onData.errback, Exception("Timeout") )
        return self.onData
    
    def openFailed(self, reason):
        print "Failed", reason
        
    @defer.inlineCallbacks    
    def channelOpen(self, ignoredData):
        self.data = ''
        self.oldData = ''
        self.onData = None
        self.timeout = None
        term = os.environ.get('TERM', 'xterm')
        #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
        winSize = (25,80,0,0) #struct.unpack('4H', winsz)
        ptyReqData = session.packRequest_pty_req(term, winSize, '')
        
        try:
            result = yield self.conn.sendRequest(self, 'pty-req', ptyReqData, wantReply = 1 )
        except Exception as e:
            print "Failed with ", e
        
        try:
            result = yield self.conn.sendRequest(self, "shell", '', wantReply = 1)
        except Exception as e:
            print "Failed shell with ", e
        
        
        #fetch preample    
        data = yield self.getResponse()
        """
        Welcome to Ubuntu 11.04 (GNU/Linux 2.6.38-8-server x86_64)

            * Documentation:  http://www.ubuntu.com/server/doc
           
             System information as of Sat Oct 29 13:09:50 MDT 2011
           
             System load:  0.0               Processes:           111
             Usage of /:   48.0% of 6.62GB   Users logged in:     1
             Memory usage: 39%               IP address for eth1: 192.168.0.19
             Swap usage:   3%
           
             Graph this data and manage this system at https://landscape.canonical.com/
           New release 'oneiric' available.
           Run 'do-release-upgrade' to upgrade to it.
           
           Last login: Sat Oct 29 01:23:16 2011 from 192.168.0.17
        """
        print data
        while data != "" and data.strip().endswith("~$") == False:
            try:
                data = yield self.getResponse()
                print repr(data)
                """
                \x1B]0;dward@pristine: ~\x07dward@pristine:~$ 
                """
            except Exception as e:
                print e
                break
                
        self.write("false\n")
        #fetch response
        try:
            data = yield self.getResponse()
        except Exception as e:
            print "Failed to catch response?", e
        else:
            print data
            """
                false
                \x1B]0;dward@pristine: ~\x07dward@pristine:~$ 
            """
            
        self.write("true\n")
        #fetch response
        try:
            data = yield self.getResponse()
        except Exception as e:
            print "Failed to catch response?", e
        else:
            print data
            """
            true
            \x1B]0;dward@pristine: ~\x07dward@pristine:~$ 
            """
        
        self.write("echo Hello World\n\x00")
        try:
            data = yield self.getResponse()
        except Exception as e:
            print "Failed to catch response?", e
        else:            
            print data
            """
            echo Hello World
            Hello World
            \x1B]0;dward@pristine: ~\x07dward@pristine:~$ 
            """
        
        #Close up shop
        self.loseConnection()
        dbgp = 1
        
    
    def request_exit_status(self, data):
        status = struct.unpack('>L', data)[0]
        print 'status was: %s' % status    
    
    def dataReceived(self, data):
        self.data += data
        if self.onData is not None:
            if self.timeout and self.timeout.active():
                self.timeout.cancel()
            if self.onData.called == False:                
                self.onData.callback(data)
    
    def extReceived(self, dataType, data):
        dbgp = 1
        print "Extended Data recieved! dataType = %s , data = %s " % ( dataType, data, )
        self.extendData = data

    def closed(self):
        print 'got data : %s' % self.data.replace("\\r\\n","\r\n")
        self.loseConnection()
        reactor.stop()
        
    

protocol.ClientCreator(reactor, SimpleTransport).connectTCP(HOST, 22)
reactor.run()

另外,我还尝试在远程shell中添加一个明确的错误命令:

    self.write("ls -alF badPathHere\n\x00")
    try:
        data = yield self.getResponse()
    except Exception as e:
        print "Failed to catch response?", e
    else:            
        print data
        """
        ls -alF badPathHere
        ls: cannot access badPathHere: No such file or directory
        \x1B]0;dward@pristine: ~\x07dward@pristine:~$ 
        """

看起来错误输出(stderr)和标准输出(stdout)混在一起了。

1 个回答

0

在研究OpenSSH的源代码时,发现频道会话的逻辑是在session.c文件的2227行,具体是在一个叫做session_input_channel_req的函数里。如果这个函数接收到一个pty-req(伪终端请求),然后再接收到一个“shell”请求,就会调用do_exec_pty,最终会调用session_set_fds(s, ptyfd, fdout, -1, 1, 1)。这里的第四个参数通常是用来处理错误输出(stderr)的文件描述符,但因为没有提供这个参数,所以错误输出就没有额外的数据了。

总的来说,即使我修改了openssh来提供一个stderr的文件描述符,问题还是出在shell上。现在只是猜测,但我认为,类似于通过像xterm或putty这样的终端登录ssh服务时,错误输出和标准输出是一起发送的,除非通过像“2> someFile”这样的方式明确重定向,这超出了SSH服务提供者的范围。

撰写回答