Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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.

session.py 13KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. import os
  10. import signal
  11. import struct
  12. import sys
  13. from zope.interface import implementer
  14. from twisted.conch.interfaces import (
  15. EnvironmentVariableNotPermitted,
  16. ISession,
  17. ISessionSetEnv,
  18. )
  19. from twisted.conch.ssh import channel, common, connection
  20. from twisted.internet import interfaces, protocol
  21. from twisted.logger import Logger
  22. from twisted.python.compat import networkString
  23. log = Logger()
  24. class SSHSession(channel.SSHChannel):
  25. """
  26. A generalized implementation of an SSH session.
  27. See RFC 4254, section 6.
  28. The precise implementation of the various operations that the remote end
  29. can send is left up to the avatar, usually via an adapter to an
  30. interface such as L{ISession}.
  31. @ivar buf: a buffer for data received before making a connection to a
  32. client.
  33. @type buf: L{bytes}
  34. @ivar client: a protocol for communication with a shell, an application
  35. program, or a subsystem (see RFC 4254, section 6.5).
  36. @type client: L{SSHSessionProcessProtocol}
  37. @ivar session: an object providing concrete implementations of session
  38. operations.
  39. @type session: L{ISession}
  40. """
  41. name = b"session"
  42. def __init__(self, *args, **kw):
  43. channel.SSHChannel.__init__(self, *args, **kw)
  44. self.buf = b""
  45. self.client = None
  46. self.session = None
  47. def request_subsystem(self, data):
  48. subsystem, ignored = common.getNS(data)
  49. log.info('Asking for subsystem "{subsystem}"', subsystem=subsystem)
  50. client = self.avatar.lookupSubsystem(subsystem, data)
  51. if client:
  52. pp = SSHSessionProcessProtocol(self)
  53. proto = wrapProcessProtocol(pp)
  54. client.makeConnection(proto)
  55. pp.makeConnection(wrapProtocol(client))
  56. self.client = pp
  57. return 1
  58. else:
  59. log.error("Failed to get subsystem")
  60. return 0
  61. def request_shell(self, data):
  62. log.info("Getting shell")
  63. if not self.session:
  64. self.session = ISession(self.avatar)
  65. try:
  66. pp = SSHSessionProcessProtocol(self)
  67. self.session.openShell(pp)
  68. except Exception:
  69. log.failure("Error getting shell")
  70. return 0
  71. else:
  72. self.client = pp
  73. return 1
  74. def request_exec(self, data):
  75. if not self.session:
  76. self.session = ISession(self.avatar)
  77. f, data = common.getNS(data)
  78. log.info('Executing command "{f}"', f=f)
  79. try:
  80. pp = SSHSessionProcessProtocol(self)
  81. self.session.execCommand(pp, f)
  82. except Exception:
  83. log.failure('Error executing command "{f}"', f=f)
  84. return 0
  85. else:
  86. self.client = pp
  87. return 1
  88. def request_pty_req(self, data):
  89. if not self.session:
  90. self.session = ISession(self.avatar)
  91. term, windowSize, modes = parseRequest_pty_req(data)
  92. log.info(
  93. "Handling pty request: {term!r} {windowSize!r}",
  94. term=term,
  95. windowSize=windowSize,
  96. )
  97. try:
  98. self.session.getPty(term, windowSize, modes)
  99. except Exception:
  100. log.failure("Error handling pty request")
  101. return 0
  102. else:
  103. return 1
  104. def request_env(self, data):
  105. """
  106. Process a request to pass an environment variable.
  107. @param data: The environment variable name and value, each encoded
  108. as an SSH protocol string and concatenated.
  109. @type data: L{bytes}
  110. @return: A true value if the request to pass this environment
  111. variable was accepted, otherwise a false value.
  112. """
  113. if not self.session:
  114. self.session = ISession(self.avatar)
  115. if not ISessionSetEnv.providedBy(self.session):
  116. return 0
  117. name, value, data = common.getNS(data, 2)
  118. try:
  119. self.session.setEnv(name, value)
  120. except EnvironmentVariableNotPermitted:
  121. return 0
  122. except Exception:
  123. log.failure("Error setting environment variable {name}", name=name)
  124. return 0
  125. else:
  126. return 1
  127. def request_window_change(self, data):
  128. if not self.session:
  129. self.session = ISession(self.avatar)
  130. winSize = parseRequest_window_change(data)
  131. try:
  132. self.session.windowChanged(winSize)
  133. except Exception:
  134. log.failure("Error changing window size")
  135. return 0
  136. else:
  137. return 1
  138. def dataReceived(self, data):
  139. if not self.client:
  140. # self.conn.sendClose(self)
  141. self.buf += data
  142. return
  143. self.client.transport.write(data)
  144. def extReceived(self, dataType, data):
  145. if dataType == connection.EXTENDED_DATA_STDERR:
  146. if self.client and hasattr(self.client.transport, "writeErr"):
  147. self.client.transport.writeErr(data)
  148. else:
  149. log.warn("Weird extended data: {dataType}", dataType=dataType)
  150. def eofReceived(self):
  151. # If we have a session, tell it that EOF has been received and
  152. # expect it to send a close message (it may need to send other
  153. # messages such as exit-status or exit-signal first). If we don't
  154. # have a session, then just send a close message directly.
  155. if self.session:
  156. self.session.eofReceived()
  157. elif self.client:
  158. self.conn.sendClose(self)
  159. def closed(self):
  160. if self.client and self.client.transport:
  161. self.client.transport.loseConnection()
  162. if self.session:
  163. self.session.closed()
  164. # def closeReceived(self):
  165. # self.loseConnection() # don't know what to do with this
  166. def loseConnection(self):
  167. if self.client:
  168. self.client.transport.loseConnection()
  169. channel.SSHChannel.loseConnection(self)
  170. class _ProtocolWrapper(protocol.ProcessProtocol):
  171. """
  172. This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
  173. """
  174. def __init__(self, proto):
  175. self.proto = proto
  176. def connectionMade(self):
  177. self.proto.connectionMade()
  178. def outReceived(self, data):
  179. self.proto.dataReceived(data)
  180. def processEnded(self, reason):
  181. self.proto.connectionLost(reason)
  182. class _DummyTransport:
  183. def __init__(self, proto):
  184. self.proto = proto
  185. def dataReceived(self, data):
  186. self.proto.transport.write(data)
  187. def write(self, data):
  188. self.proto.dataReceived(data)
  189. def writeSequence(self, seq):
  190. self.write(b"".join(seq))
  191. def loseConnection(self):
  192. self.proto.connectionLost(protocol.connectionDone)
  193. def wrapProcessProtocol(inst):
  194. if isinstance(inst, protocol.Protocol):
  195. return _ProtocolWrapper(inst)
  196. else:
  197. return inst
  198. def wrapProtocol(proto):
  199. return _DummyTransport(proto)
  200. # SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
  201. # to accept. See RFC 4254
  202. SUPPORTED_SIGNALS = [
  203. "ABRT",
  204. "ALRM",
  205. "FPE",
  206. "HUP",
  207. "ILL",
  208. "INT",
  209. "KILL",
  210. "PIPE",
  211. "QUIT",
  212. "SEGV",
  213. "TERM",
  214. "USR1",
  215. "USR2",
  216. ]
  217. @implementer(interfaces.ITransport)
  218. class SSHSessionProcessProtocol(protocol.ProcessProtocol):
  219. """I am both an L{IProcessProtocol} and an L{ITransport}.
  220. I am a transport to the remote endpoint and a process protocol to the
  221. local subsystem.
  222. """
  223. # once initialized, a dictionary mapping signal values to strings
  224. # that follow RFC 4254.
  225. _signalValuesToNames = None
  226. def __init__(self, session):
  227. self.session = session
  228. self.lostOutOrErrFlag = False
  229. def connectionMade(self):
  230. if self.session.buf:
  231. self.transport.write(self.session.buf)
  232. self.session.buf = None
  233. def outReceived(self, data):
  234. self.session.write(data)
  235. def errReceived(self, err):
  236. self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
  237. def outConnectionLost(self):
  238. """
  239. EOF should only be sent when both STDOUT and STDERR have been closed.
  240. """
  241. if self.lostOutOrErrFlag:
  242. self.session.conn.sendEOF(self.session)
  243. else:
  244. self.lostOutOrErrFlag = True
  245. def errConnectionLost(self):
  246. """
  247. See outConnectionLost().
  248. """
  249. self.outConnectionLost()
  250. def connectionLost(self, reason=None):
  251. self.session.loseConnection()
  252. def _getSignalName(self, signum):
  253. """
  254. Get a signal name given a signal number.
  255. """
  256. if self._signalValuesToNames is None:
  257. self._signalValuesToNames = {}
  258. # make sure that the POSIX ones are the defaults
  259. for signame in SUPPORTED_SIGNALS:
  260. signame = "SIG" + signame
  261. sigvalue = getattr(signal, signame, None)
  262. if sigvalue is not None:
  263. self._signalValuesToNames[sigvalue] = signame
  264. for k, v in signal.__dict__.items():
  265. # Check for platform specific signals, ignoring Python specific
  266. # SIG_DFL and SIG_IGN
  267. if k.startswith("SIG") and not k.startswith("SIG_"):
  268. if v not in self._signalValuesToNames:
  269. self._signalValuesToNames[v] = k + "@" + sys.platform
  270. return self._signalValuesToNames[signum]
  271. def processEnded(self, reason=None):
  272. """
  273. When we are told the process ended, try to notify the other side about
  274. how the process ended using the exit-signal or exit-status requests.
  275. Also, close the channel.
  276. """
  277. if reason is not None:
  278. err = reason.value
  279. if err.signal is not None:
  280. signame = self._getSignalName(err.signal)
  281. if getattr(os, "WCOREDUMP", None) is not None and os.WCOREDUMP(
  282. err.status
  283. ):
  284. log.info("exitSignal: {signame} (core dumped)", signame=signame)
  285. coreDumped = True
  286. else:
  287. log.info("exitSignal: {}", signame=signame)
  288. coreDumped = False
  289. self.session.conn.sendRequest(
  290. self.session,
  291. b"exit-signal",
  292. common.NS(networkString(signame[3:]))
  293. + (b"\1" if coreDumped else b"\0")
  294. + common.NS(b"")
  295. + common.NS(b""),
  296. )
  297. elif err.exitCode is not None:
  298. log.info("exitCode: {exitCode!r}", exitCode=err.exitCode)
  299. self.session.conn.sendRequest(
  300. self.session, b"exit-status", struct.pack(">L", err.exitCode)
  301. )
  302. self.session.loseConnection()
  303. def getHost(self):
  304. """
  305. Return the host from my session's transport.
  306. """
  307. return self.session.conn.transport.getHost()
  308. def getPeer(self):
  309. """
  310. Return the peer from my session's transport.
  311. """
  312. return self.session.conn.transport.getPeer()
  313. def write(self, data):
  314. self.session.write(data)
  315. def writeSequence(self, seq):
  316. self.session.write(b"".join(seq))
  317. def loseConnection(self):
  318. self.session.loseConnection()
  319. class SSHSessionClient(protocol.Protocol):
  320. def dataReceived(self, data):
  321. if self.transport:
  322. self.transport.write(data)
  323. # methods factored out to make live easier on server writers
  324. def parseRequest_pty_req(data):
  325. """Parse the data from a pty-req request into usable data.
  326. @returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
  327. """
  328. term, rest = common.getNS(data)
  329. cols, rows, xpixel, ypixel = struct.unpack(">4L", rest[:16])
  330. modes, ignored = common.getNS(rest[16:])
  331. winSize = (rows, cols, xpixel, ypixel)
  332. modes = [
  333. (ord(modes[i : i + 1]), struct.unpack(">L", modes[i + 1 : i + 5])[0])
  334. for i in range(0, len(modes) - 1, 5)
  335. ]
  336. return term, winSize, modes
  337. def packRequest_pty_req(term, geometry, modes):
  338. """
  339. Pack a pty-req request so that it is suitable for sending.
  340. NOTE: modes must be packed before being sent here.
  341. @type geometry: L{tuple}
  342. @param geometry: A tuple of (rows, columns, xpixel, ypixel)
  343. """
  344. (rows, cols, xpixel, ypixel) = geometry
  345. termPacked = common.NS(term)
  346. winSizePacked = struct.pack(">4L", cols, rows, xpixel, ypixel)
  347. modesPacked = common.NS(modes) # depend on the client packing modes
  348. return termPacked + winSizePacked + modesPacked
  349. def parseRequest_window_change(data):
  350. """Parse the data from a window-change request into usuable data.
  351. @returns: a tuple of (rows, cols, xpixel, ypixel)
  352. """
  353. cols, rows, xpixel, ypixel = struct.unpack(">4L", data)
  354. return rows, cols, xpixel, ypixel
  355. def packRequest_window_change(geometry):
  356. """
  357. Pack a window-change request so that it is suitable for sending.
  358. @type geometry: L{tuple}
  359. @param geometry: A tuple of (rows, columns, xpixel, ypixel)
  360. """
  361. (rows, cols, xpixel, ypixel) = geometry
  362. return struct.pack(">4L", cols, rows, xpixel, ypixel)