session.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. # -*- test-case-name: twisted.conch.test.test_session -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. This module contains the implementation of SSHSession, which (by default)
  6. allows access to a shell and a python interpreter over SSH.
  7. Maintainer: Paul Swartz
  8. """
  9. from __future__ import division, absolute_import
  10. import struct
  11. import signal
  12. import sys
  13. import os
  14. from zope.interface import implementer
  15. from twisted.internet import interfaces, protocol
  16. from twisted.python import log
  17. from twisted.python.compat import _bytesChr as chr, networkString
  18. from twisted.conch.interfaces import ISession
  19. from twisted.conch.ssh import common, channel, connection
  20. class SSHSession(channel.SSHChannel):
  21. name = b'session'
  22. def __init__(self, *args, **kw):
  23. channel.SSHChannel.__init__(self, *args, **kw)
  24. self.buf = b''
  25. self.client = None
  26. self.session = None
  27. def request_subsystem(self, data):
  28. subsystem, ignored= common.getNS(data)
  29. log.msg('asking for subsystem "%s"' % subsystem)
  30. client = self.avatar.lookupSubsystem(subsystem, data)
  31. if client:
  32. pp = SSHSessionProcessProtocol(self)
  33. proto = wrapProcessProtocol(pp)
  34. client.makeConnection(proto)
  35. pp.makeConnection(wrapProtocol(client))
  36. self.client = pp
  37. return 1
  38. else:
  39. log.msg('failed to get subsystem')
  40. return 0
  41. def request_shell(self, data):
  42. log.msg('getting shell')
  43. if not self.session:
  44. self.session = ISession(self.avatar)
  45. try:
  46. pp = SSHSessionProcessProtocol(self)
  47. self.session.openShell(pp)
  48. except:
  49. log.deferr()
  50. return 0
  51. else:
  52. self.client = pp
  53. return 1
  54. def request_exec(self, data):
  55. if not self.session:
  56. self.session = ISession(self.avatar)
  57. f,data = common.getNS(data)
  58. log.msg('executing command "%s"' % f)
  59. try:
  60. pp = SSHSessionProcessProtocol(self)
  61. self.session.execCommand(pp, f)
  62. except:
  63. log.deferr()
  64. return 0
  65. else:
  66. self.client = pp
  67. return 1
  68. def request_pty_req(self, data):
  69. if not self.session:
  70. self.session = ISession(self.avatar)
  71. term, windowSize, modes = parseRequest_pty_req(data)
  72. log.msg('pty request: %r %r' % (term, windowSize))
  73. try:
  74. self.session.getPty(term, windowSize, modes)
  75. except:
  76. log.err()
  77. return 0
  78. else:
  79. return 1
  80. def request_window_change(self, data):
  81. if not self.session:
  82. self.session = ISession(self.avatar)
  83. winSize = parseRequest_window_change(data)
  84. try:
  85. self.session.windowChanged(winSize)
  86. except:
  87. log.msg('error changing window size')
  88. log.err()
  89. return 0
  90. else:
  91. return 1
  92. def dataReceived(self, data):
  93. if not self.client:
  94. #self.conn.sendClose(self)
  95. self.buf += data
  96. return
  97. self.client.transport.write(data)
  98. def extReceived(self, dataType, data):
  99. if dataType == connection.EXTENDED_DATA_STDERR:
  100. if self.client and hasattr(self.client.transport, 'writeErr'):
  101. self.client.transport.writeErr(data)
  102. else:
  103. log.msg('weird extended data: %s'%dataType)
  104. def eofReceived(self):
  105. if self.session:
  106. self.session.eofReceived()
  107. elif self.client:
  108. self.conn.sendClose(self)
  109. def closed(self):
  110. if self.session:
  111. self.session.closed()
  112. elif self.client:
  113. self.client.transport.loseConnection()
  114. #def closeReceived(self):
  115. # self.loseConnection() # don't know what to do with this
  116. def loseConnection(self):
  117. if self.client:
  118. self.client.transport.loseConnection()
  119. channel.SSHChannel.loseConnection(self)
  120. class _ProtocolWrapper(protocol.ProcessProtocol):
  121. """
  122. This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
  123. """
  124. def __init__(self, proto):
  125. self.proto = proto
  126. def connectionMade(self): self.proto.connectionMade()
  127. def outReceived(self, data): self.proto.dataReceived(data)
  128. def processEnded(self, reason): self.proto.connectionLost(reason)
  129. class _DummyTransport:
  130. def __init__(self, proto):
  131. self.proto = proto
  132. def dataReceived(self, data):
  133. self.proto.transport.write(data)
  134. def write(self, data):
  135. self.proto.dataReceived(data)
  136. def writeSequence(self, seq):
  137. self.write(b''.join(seq))
  138. def loseConnection(self):
  139. self.proto.connectionLost(protocol.connectionDone)
  140. def wrapProcessProtocol(inst):
  141. if isinstance(inst, protocol.Protocol):
  142. return _ProtocolWrapper(inst)
  143. else:
  144. return inst
  145. def wrapProtocol(proto):
  146. return _DummyTransport(proto)
  147. # SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
  148. # to accept. See RFC 4254
  149. SUPPORTED_SIGNALS = ["ABRT", "ALRM", "FPE", "HUP", "ILL", "INT", "KILL",
  150. "PIPE", "QUIT", "SEGV", "TERM", "USR1", "USR2"]
  151. @implementer(interfaces.ITransport)
  152. class SSHSessionProcessProtocol(protocol.ProcessProtocol):
  153. """I am both an L{IProcessProtocol} and an L{ITransport}.
  154. I am a transport to the remote endpoint and a process protocol to the
  155. local subsystem.
  156. """
  157. # once initialized, a dictionary mapping signal values to strings
  158. # that follow RFC 4254.
  159. _signalValuesToNames = None
  160. def __init__(self, session):
  161. self.session = session
  162. self.lostOutOrErrFlag = False
  163. def connectionMade(self):
  164. if self.session.buf:
  165. self.transport.write(self.session.buf)
  166. self.session.buf = None
  167. def outReceived(self, data):
  168. self.session.write(data)
  169. def errReceived(self, err):
  170. self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
  171. def outConnectionLost(self):
  172. """
  173. EOF should only be sent when both STDOUT and STDERR have been closed.
  174. """
  175. if self.lostOutOrErrFlag:
  176. self.session.conn.sendEOF(self.session)
  177. else:
  178. self.lostOutOrErrFlag = True
  179. def errConnectionLost(self):
  180. """
  181. See outConnectionLost().
  182. """
  183. self.outConnectionLost()
  184. def connectionLost(self, reason = None):
  185. self.session.loseConnection()
  186. def _getSignalName(self, signum):
  187. """
  188. Get a signal name given a signal number.
  189. """
  190. if self._signalValuesToNames is None:
  191. self._signalValuesToNames = {}
  192. # make sure that the POSIX ones are the defaults
  193. for signame in SUPPORTED_SIGNALS:
  194. signame = 'SIG' + signame
  195. sigvalue = getattr(signal, signame, None)
  196. if sigvalue is not None:
  197. self._signalValuesToNames[sigvalue] = signame
  198. for k, v in signal.__dict__.items():
  199. # Check for platform specific signals, ignoring Python specific
  200. # SIG_DFL and SIG_IGN
  201. if k.startswith('SIG') and not k.startswith('SIG_'):
  202. if v not in self._signalValuesToNames:
  203. self._signalValuesToNames[v] = k + '@' + sys.platform
  204. return self._signalValuesToNames[signum]
  205. def processEnded(self, reason=None):
  206. """
  207. When we are told the process ended, try to notify the other side about
  208. how the process ended using the exit-signal or exit-status requests.
  209. Also, close the channel.
  210. """
  211. if reason is not None:
  212. err = reason.value
  213. if err.signal is not None:
  214. signame = self._getSignalName(err.signal)
  215. if (getattr(os, 'WCOREDUMP', None) is not None and
  216. os.WCOREDUMP(err.status)):
  217. log.msg('exitSignal: %s (core dumped)' % (signame,))
  218. coreDumped = 1
  219. else:
  220. log.msg('exitSignal: %s' % (signame,))
  221. coreDumped = 0
  222. self.session.conn.sendRequest(
  223. self.session, b'exit-signal',
  224. common.NS(networkString(signame[3:])) + chr(coreDumped) +
  225. common.NS(b'') + common.NS(b''))
  226. elif err.exitCode is not None:
  227. log.msg('exitCode: %r' % (err.exitCode,))
  228. self.session.conn.sendRequest(self.session, b'exit-status',
  229. struct.pack('>L', err.exitCode))
  230. self.session.loseConnection()
  231. def getHost(self):
  232. """
  233. Return the host from my session's transport.
  234. """
  235. return self.session.conn.transport.getHost()
  236. def getPeer(self):
  237. """
  238. Return the peer from my session's transport.
  239. """
  240. return self.session.conn.transport.getPeer()
  241. def write(self, data):
  242. self.session.write(data)
  243. def writeSequence(self, seq):
  244. self.session.write(b''.join(seq))
  245. def loseConnection(self):
  246. self.session.loseConnection()
  247. class SSHSessionClient(protocol.Protocol):
  248. def dataReceived(self, data):
  249. if self.transport:
  250. self.transport.write(data)
  251. # methods factored out to make live easier on server writers
  252. def parseRequest_pty_req(data):
  253. """Parse the data from a pty-req request into usable data.
  254. @returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
  255. """
  256. term, rest = common.getNS(data)
  257. cols, rows, xpixel, ypixel = struct.unpack('>4L', rest[: 16])
  258. modes, ignored= common.getNS(rest[16:])
  259. winSize = (rows, cols, xpixel, ypixel)
  260. modes = [(ord(modes[i:i+1]), struct.unpack('>L', modes[i+1: i+5])[0])
  261. for i in range(0, len(modes)-1, 5)]
  262. return term, winSize, modes
  263. def packRequest_pty_req(term, geometry, modes):
  264. """
  265. Pack a pty-req request so that it is suitable for sending.
  266. NOTE: modes must be packed before being sent here.
  267. @type geometry: L{tuple}
  268. @param geometry: A tuple of (rows, columns, xpixel, ypixel)
  269. """
  270. (rows, cols, xpixel, ypixel) = geometry
  271. termPacked = common.NS(term)
  272. winSizePacked = struct.pack('>4L', cols, rows, xpixel, ypixel)
  273. modesPacked = common.NS(modes) # depend on the client packing modes
  274. return termPacked + winSizePacked + modesPacked
  275. def parseRequest_window_change(data):
  276. """Parse the data from a window-change request into usuable data.
  277. @returns: a tuple of (rows, cols, xpixel, ypixel)
  278. """
  279. cols, rows, xpixel, ypixel = struct.unpack('>4L', data)
  280. return rows, cols, xpixel, ypixel
  281. def packRequest_window_change(geometry):
  282. """
  283. Pack a window-change request so that it is suitable for sending.
  284. @type geometry: L{tuple}
  285. @param geometry: A tuple of (rows, columns, xpixel, ypixel)
  286. """
  287. (rows, cols, xpixel, ypixel) = geometry
  288. return struct.pack('>4L', cols, rows, xpixel, ypixel)