backdoor.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # Copyright (c) 2009-2014, gevent contributors
  2. # Based on eventlet.backdoor Copyright (c) 2005-2006, Bob Ippolito
  3. """
  4. Interactive greenlet-based network console that can be used in any process.
  5. The :class:`BackdoorServer` provides a REPL inside a running process. As
  6. long as the process is monkey-patched, the ``BackdoorServer`` can coexist
  7. with other elements of the process.
  8. .. seealso:: :class:`code.InteractiveConsole`
  9. """
  10. from __future__ import print_function, absolute_import
  11. import sys
  12. from code import InteractiveConsole
  13. from gevent.greenlet import Greenlet
  14. from gevent.hub import getcurrent
  15. from gevent.server import StreamServer
  16. from gevent.pool import Pool
  17. __all__ = ['BackdoorServer']
  18. try:
  19. sys.ps1
  20. except AttributeError:
  21. sys.ps1 = '>>> '
  22. try:
  23. sys.ps2
  24. except AttributeError:
  25. sys.ps2 = '... '
  26. class _Greenlet_stdreplace(Greenlet):
  27. # A greenlet that replaces sys.std[in/out/err] while running.
  28. _fileobj = None
  29. saved = None
  30. def switch(self, *args, **kw):
  31. if self._fileobj is not None:
  32. self.switch_in()
  33. Greenlet.switch(self, *args, **kw)
  34. def switch_in(self):
  35. self.saved = sys.stdin, sys.stderr, sys.stdout
  36. sys.stdin = sys.stdout = sys.stderr = self._fileobj
  37. def switch_out(self):
  38. sys.stdin, sys.stderr, sys.stdout = self.saved
  39. self.saved = None
  40. def throw(self, *args, **kwargs):
  41. # pylint:disable=arguments-differ
  42. if self.saved is None and self._fileobj is not None:
  43. self.switch_in()
  44. Greenlet.throw(self, *args, **kwargs)
  45. def run(self):
  46. try:
  47. return Greenlet.run(self)
  48. finally:
  49. # Make sure to restore the originals.
  50. self.switch_out()
  51. class BackdoorServer(StreamServer):
  52. """
  53. Provide a backdoor to a program for debugging purposes.
  54. .. warning:: This backdoor provides no authentication and makes no
  55. attempt to limit what remote users can do. Anyone that
  56. can access the server can take any action that the running
  57. python process can. Thus, while you may bind to any interface, for
  58. security purposes it is recommended that you bind to one
  59. only accessible to the local machine, e.g.,
  60. 127.0.0.1/localhost.
  61. Basic usage::
  62. from gevent.backdoor import BackdoorServer
  63. server = BackdoorServer(('127.0.0.1', 5001),
  64. banner="Hello from gevent backdoor!",
  65. locals={'foo': "From defined scope!"})
  66. server.serve_forever()
  67. In a another terminal, connect with...::
  68. $ telnet 127.0.0.1 5001
  69. Trying 127.0.0.1...
  70. Connected to 127.0.0.1.
  71. Escape character is '^]'.
  72. Hello from gevent backdoor!
  73. >> print(foo)
  74. From defined scope!
  75. .. versionchanged:: 1.2a1
  76. Spawned greenlets are now tracked in a pool and killed when the server
  77. is stopped.
  78. """
  79. def __init__(self, listener, locals=None, banner=None, **server_args):
  80. """
  81. :keyword locals: If given, a dictionary of "builtin" values that will be available
  82. at the top-level.
  83. :keyword banner: If geven, a string that will be printed to each connecting user.
  84. """
  85. group = Pool(greenlet_class=_Greenlet_stdreplace) # no limit on number
  86. StreamServer.__init__(self, listener, spawn=group, **server_args)
  87. _locals = {'__doc__': None, '__name__': '__console__'}
  88. if locals:
  89. _locals.update(locals)
  90. self.locals = _locals
  91. self.banner = banner
  92. self.stderr = sys.stderr
  93. def _create_interactive_locals(self):
  94. # Create and return a *new* locals dictionary based on self.locals,
  95. # and set any new entries in it. (InteractiveConsole does not
  96. # copy its locals value)
  97. _locals = self.locals.copy()
  98. # __builtins__ may either be the __builtin__ module or
  99. # __builtin__.__dict__; in the latter case typing
  100. # locals() at the backdoor prompt spews out lots of
  101. # useless stuff
  102. try:
  103. import __builtin__
  104. _locals["__builtins__"] = __builtin__
  105. except ImportError:
  106. import builtins # pylint:disable=import-error
  107. _locals["builtins"] = builtins
  108. _locals['__builtins__'] = builtins
  109. return _locals
  110. def handle(self, conn, _address): # pylint: disable=method-hidden
  111. """
  112. Interact with one remote user.
  113. .. versionchanged:: 1.1b2 Each connection gets its own
  114. ``locals`` dictionary. Previously they were shared in a
  115. potentially unsafe manner.
  116. """
  117. fobj = conn.makefile(mode="rw")
  118. fobj = _fileobject(conn, fobj, self.stderr)
  119. getcurrent()._fileobj = fobj
  120. getcurrent().switch_in()
  121. try:
  122. console = InteractiveConsole(self._create_interactive_locals())
  123. if sys.version_info[:3] >= (3, 6, 0):
  124. # Beginning in 3.6, the console likes to print "now exiting <class>"
  125. # but probably our socket is already closed, so this just causes problems.
  126. console.interact(banner=self.banner, exitmsg='') # pylint:disable=unexpected-keyword-arg
  127. else:
  128. console.interact(banner=self.banner)
  129. except SystemExit: # raised by quit()
  130. if hasattr(sys, 'exc_clear'): # py2
  131. sys.exc_clear()
  132. finally:
  133. conn.close()
  134. fobj.close()
  135. class _fileobject(object):
  136. """
  137. A file-like object that wraps the result of socket.makefile (composition
  138. instead of inheritance lets us work identically under CPython and PyPy).
  139. We write directly to the socket, avoiding the buffering that the text-oriented
  140. makefile would want to do (otherwise we'd be at the mercy of waiting on a
  141. flush() to get called for the remote user to see data); this beats putting
  142. the file in binary mode and translating everywhere with a non-default
  143. encoding.
  144. """
  145. def __init__(self, sock, fobj, stderr):
  146. self._sock = sock
  147. self._fobj = fobj
  148. self.stderr = stderr
  149. def __getattr__(self, name):
  150. return getattr(self._fobj, name)
  151. def write(self, data):
  152. if not isinstance(data, bytes):
  153. data = data.encode('utf-8')
  154. self._sock.sendall(data)
  155. def isatty(self):
  156. return True
  157. def flush(self):
  158. pass
  159. def readline(self, *a):
  160. try:
  161. return self._fobj.readline(*a).replace("\r\n", "\n")
  162. except UnicodeError:
  163. # Typically, under python 3, a ^C on the other end
  164. return ''
  165. if __name__ == '__main__':
  166. if not sys.argv[1:]:
  167. print('USAGE: %s PORT [banner]' % sys.argv[0])
  168. else:
  169. BackdoorServer(('127.0.0.1', int(sys.argv[1])),
  170. banner=(sys.argv[2] if len(sys.argv) > 2 else None),
  171. locals={'hello': 'world'}).serve_forever()