123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- # -*- test-case-name: twisted.conch.test.test_session -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- This module contains the implementation of SSHSession, which (by default)
- allows access to a shell and a python interpreter over SSH.
-
- Maintainer: Paul Swartz
- """
-
-
- import os
- import signal
- import struct
- import sys
-
- from zope.interface import implementer
-
- from twisted.conch.interfaces import (
- EnvironmentVariableNotPermitted,
- ISession,
- ISessionSetEnv,
- )
- from twisted.conch.ssh import channel, common, connection
- from twisted.internet import interfaces, protocol
- from twisted.logger import Logger
- from twisted.python.compat import networkString
-
- log = Logger()
-
-
- class SSHSession(channel.SSHChannel):
- """
- A generalized implementation of an SSH session.
-
- See RFC 4254, section 6.
-
- The precise implementation of the various operations that the remote end
- can send is left up to the avatar, usually via an adapter to an
- interface such as L{ISession}.
-
- @ivar buf: a buffer for data received before making a connection to a
- client.
- @type buf: L{bytes}
- @ivar client: a protocol for communication with a shell, an application
- program, or a subsystem (see RFC 4254, section 6.5).
- @type client: L{SSHSessionProcessProtocol}
- @ivar session: an object providing concrete implementations of session
- operations.
- @type session: L{ISession}
- """
-
- name = b"session"
-
- def __init__(self, *args, **kw):
- channel.SSHChannel.__init__(self, *args, **kw)
- self.buf = b""
- self.client = None
- self.session = None
-
- def request_subsystem(self, data):
- subsystem, ignored = common.getNS(data)
- log.info('Asking for subsystem "{subsystem}"', subsystem=subsystem)
- client = self.avatar.lookupSubsystem(subsystem, data)
- if client:
- pp = SSHSessionProcessProtocol(self)
- proto = wrapProcessProtocol(pp)
- client.makeConnection(proto)
- pp.makeConnection(wrapProtocol(client))
- self.client = pp
- return 1
- else:
- log.error("Failed to get subsystem")
- return 0
-
- def request_shell(self, data):
- log.info("Getting shell")
- if not self.session:
- self.session = ISession(self.avatar)
- try:
- pp = SSHSessionProcessProtocol(self)
- self.session.openShell(pp)
- except Exception:
- log.failure("Error getting shell")
- return 0
- else:
- self.client = pp
- return 1
-
- def request_exec(self, data):
- if not self.session:
- self.session = ISession(self.avatar)
- f, data = common.getNS(data)
- log.info('Executing command "{f}"', f=f)
- try:
- pp = SSHSessionProcessProtocol(self)
- self.session.execCommand(pp, f)
- except Exception:
- log.failure('Error executing command "{f}"', f=f)
- return 0
- else:
- self.client = pp
- return 1
-
- def request_pty_req(self, data):
- if not self.session:
- self.session = ISession(self.avatar)
- term, windowSize, modes = parseRequest_pty_req(data)
- log.info(
- "Handling pty request: {term!r} {windowSize!r}",
- term=term,
- windowSize=windowSize,
- )
- try:
- self.session.getPty(term, windowSize, modes)
- except Exception:
- log.failure("Error handling pty request")
- return 0
- else:
- return 1
-
- def request_env(self, data):
- """
- Process a request to pass an environment variable.
-
- @param data: The environment variable name and value, each encoded
- as an SSH protocol string and concatenated.
- @type data: L{bytes}
- @return: A true value if the request to pass this environment
- variable was accepted, otherwise a false value.
- """
- if not self.session:
- self.session = ISession(self.avatar)
- if not ISessionSetEnv.providedBy(self.session):
- return 0
- name, value, data = common.getNS(data, 2)
- try:
- self.session.setEnv(name, value)
- except EnvironmentVariableNotPermitted:
- return 0
- except Exception:
- log.failure("Error setting environment variable {name}", name=name)
- return 0
- else:
- return 1
-
- def request_window_change(self, data):
- if not self.session:
- self.session = ISession(self.avatar)
- winSize = parseRequest_window_change(data)
- try:
- self.session.windowChanged(winSize)
- except Exception:
- log.failure("Error changing window size")
- return 0
- else:
- return 1
-
- def dataReceived(self, data):
- if not self.client:
- # self.conn.sendClose(self)
- self.buf += data
- return
- self.client.transport.write(data)
-
- def extReceived(self, dataType, data):
- if dataType == connection.EXTENDED_DATA_STDERR:
- if self.client and hasattr(self.client.transport, "writeErr"):
- self.client.transport.writeErr(data)
- else:
- log.warn("Weird extended data: {dataType}", dataType=dataType)
-
- def eofReceived(self):
- # If we have a session, tell it that EOF has been received and
- # expect it to send a close message (it may need to send other
- # messages such as exit-status or exit-signal first). If we don't
- # have a session, then just send a close message directly.
- if self.session:
- self.session.eofReceived()
- elif self.client:
- self.conn.sendClose(self)
-
- def closed(self):
- if self.client and self.client.transport:
- self.client.transport.loseConnection()
- if self.session:
- self.session.closed()
-
- # def closeReceived(self):
- # self.loseConnection() # don't know what to do with this
-
- def loseConnection(self):
- if self.client:
- self.client.transport.loseConnection()
- channel.SSHChannel.loseConnection(self)
-
-
- class _ProtocolWrapper(protocol.ProcessProtocol):
- """
- This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
- """
-
- def __init__(self, proto):
- self.proto = proto
-
- def connectionMade(self):
- self.proto.connectionMade()
-
- def outReceived(self, data):
- self.proto.dataReceived(data)
-
- def processEnded(self, reason):
- self.proto.connectionLost(reason)
-
-
- class _DummyTransport:
- def __init__(self, proto):
- self.proto = proto
-
- def dataReceived(self, data):
- self.proto.transport.write(data)
-
- def write(self, data):
- self.proto.dataReceived(data)
-
- def writeSequence(self, seq):
- self.write(b"".join(seq))
-
- def loseConnection(self):
- self.proto.connectionLost(protocol.connectionDone)
-
-
- def wrapProcessProtocol(inst):
- if isinstance(inst, protocol.Protocol):
- return _ProtocolWrapper(inst)
- else:
- return inst
-
-
- def wrapProtocol(proto):
- return _DummyTransport(proto)
-
-
- # SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
- # to accept. See RFC 4254
- SUPPORTED_SIGNALS = [
- "ABRT",
- "ALRM",
- "FPE",
- "HUP",
- "ILL",
- "INT",
- "KILL",
- "PIPE",
- "QUIT",
- "SEGV",
- "TERM",
- "USR1",
- "USR2",
- ]
-
-
- @implementer(interfaces.ITransport)
- class SSHSessionProcessProtocol(protocol.ProcessProtocol):
- """I am both an L{IProcessProtocol} and an L{ITransport}.
-
- I am a transport to the remote endpoint and a process protocol to the
- local subsystem.
- """
-
- # once initialized, a dictionary mapping signal values to strings
- # that follow RFC 4254.
- _signalValuesToNames = None
-
- def __init__(self, session):
- self.session = session
- self.lostOutOrErrFlag = False
-
- def connectionMade(self):
- if self.session.buf:
- self.transport.write(self.session.buf)
- self.session.buf = None
-
- def outReceived(self, data):
- self.session.write(data)
-
- def errReceived(self, err):
- self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
-
- def outConnectionLost(self):
- """
- EOF should only be sent when both STDOUT and STDERR have been closed.
- """
- if self.lostOutOrErrFlag:
- self.session.conn.sendEOF(self.session)
- else:
- self.lostOutOrErrFlag = True
-
- def errConnectionLost(self):
- """
- See outConnectionLost().
- """
- self.outConnectionLost()
-
- def connectionLost(self, reason=None):
- self.session.loseConnection()
-
- def _getSignalName(self, signum):
- """
- Get a signal name given a signal number.
- """
- if self._signalValuesToNames is None:
- self._signalValuesToNames = {}
- # make sure that the POSIX ones are the defaults
- for signame in SUPPORTED_SIGNALS:
- signame = "SIG" + signame
- sigvalue = getattr(signal, signame, None)
- if sigvalue is not None:
- self._signalValuesToNames[sigvalue] = signame
- for k, v in signal.__dict__.items():
- # Check for platform specific signals, ignoring Python specific
- # SIG_DFL and SIG_IGN
- if k.startswith("SIG") and not k.startswith("SIG_"):
- if v not in self._signalValuesToNames:
- self._signalValuesToNames[v] = k + "@" + sys.platform
- return self._signalValuesToNames[signum]
-
- def processEnded(self, reason=None):
- """
- When we are told the process ended, try to notify the other side about
- how the process ended using the exit-signal or exit-status requests.
- Also, close the channel.
- """
- if reason is not None:
- err = reason.value
- if err.signal is not None:
- signame = self._getSignalName(err.signal)
- if getattr(os, "WCOREDUMP", None) is not None and os.WCOREDUMP(
- err.status
- ):
- log.info("exitSignal: {signame} (core dumped)", signame=signame)
- coreDumped = True
- else:
- log.info("exitSignal: {}", signame=signame)
- coreDumped = False
- self.session.conn.sendRequest(
- self.session,
- b"exit-signal",
- common.NS(networkString(signame[3:]))
- + (b"\1" if coreDumped else b"\0")
- + common.NS(b"")
- + common.NS(b""),
- )
- elif err.exitCode is not None:
- log.info("exitCode: {exitCode!r}", exitCode=err.exitCode)
- self.session.conn.sendRequest(
- self.session, b"exit-status", struct.pack(">L", err.exitCode)
- )
- self.session.loseConnection()
-
- def getHost(self):
- """
- Return the host from my session's transport.
- """
- return self.session.conn.transport.getHost()
-
- def getPeer(self):
- """
- Return the peer from my session's transport.
- """
- return self.session.conn.transport.getPeer()
-
- def write(self, data):
- self.session.write(data)
-
- def writeSequence(self, seq):
- self.session.write(b"".join(seq))
-
- def loseConnection(self):
- self.session.loseConnection()
-
-
- class SSHSessionClient(protocol.Protocol):
- def dataReceived(self, data):
- if self.transport:
- self.transport.write(data)
-
-
- # methods factored out to make live easier on server writers
- def parseRequest_pty_req(data):
- """Parse the data from a pty-req request into usable data.
-
- @returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
- """
- term, rest = common.getNS(data)
- cols, rows, xpixel, ypixel = struct.unpack(">4L", rest[:16])
- modes, ignored = common.getNS(rest[16:])
- winSize = (rows, cols, xpixel, ypixel)
- modes = [
- (ord(modes[i : i + 1]), struct.unpack(">L", modes[i + 1 : i + 5])[0])
- for i in range(0, len(modes) - 1, 5)
- ]
- return term, winSize, modes
-
-
- def packRequest_pty_req(term, geometry, modes):
- """
- Pack a pty-req request so that it is suitable for sending.
-
- NOTE: modes must be packed before being sent here.
-
- @type geometry: L{tuple}
- @param geometry: A tuple of (rows, columns, xpixel, ypixel)
- """
- (rows, cols, xpixel, ypixel) = geometry
- termPacked = common.NS(term)
- winSizePacked = struct.pack(">4L", cols, rows, xpixel, ypixel)
- modesPacked = common.NS(modes) # depend on the client packing modes
- return termPacked + winSizePacked + modesPacked
-
-
- def parseRequest_window_change(data):
- """Parse the data from a window-change request into usuable data.
-
- @returns: a tuple of (rows, cols, xpixel, ypixel)
- """
- cols, rows, xpixel, ypixel = struct.unpack(">4L", data)
- return rows, cols, xpixel, ypixel
-
-
- def packRequest_window_change(geometry):
- """
- Pack a window-change request so that it is suitable for sending.
-
- @type geometry: L{tuple}
- @param geometry: A tuple of (rows, columns, xpixel, ypixel)
- """
- (rows, cols, xpixel, ypixel) = geometry
- return struct.pack(">4L", cols, rows, xpixel, ypixel)
|