diff options
Diffstat (limited to 'python/gevent/backdoor.py')
-rw-r--r-- | python/gevent/backdoor.py | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/python/gevent/backdoor.py b/python/gevent/backdoor.py new file mode 100644 index 0000000..ff45166 --- /dev/null +++ b/python/gevent/backdoor.py @@ -0,0 +1,206 @@ +# Copyright (c) 2009-2014, gevent contributors +# Based on eventlet.backdoor Copyright (c) 2005-2006, Bob Ippolito +""" +Interactive greenlet-based network console that can be used in any process. + +The :class:`BackdoorServer` provides a REPL inside a running process. As +long as the process is monkey-patched, the ``BackdoorServer`` can coexist +with other elements of the process. + +.. seealso:: :class:`code.InteractiveConsole` +""" +from __future__ import print_function, absolute_import +import sys +from code import InteractiveConsole + +from gevent.greenlet import Greenlet +from gevent.hub import getcurrent +from gevent.server import StreamServer +from gevent.pool import Pool + +__all__ = ['BackdoorServer'] + +try: + sys.ps1 +except AttributeError: + sys.ps1 = '>>> ' +try: + sys.ps2 +except AttributeError: + sys.ps2 = '... ' + +class _Greenlet_stdreplace(Greenlet): + # A greenlet that replaces sys.std[in/out/err] while running. + _fileobj = None + saved = None + + def switch(self, *args, **kw): + if self._fileobj is not None: + self.switch_in() + Greenlet.switch(self, *args, **kw) + + def switch_in(self): + self.saved = sys.stdin, sys.stderr, sys.stdout + sys.stdin = sys.stdout = sys.stderr = self._fileobj + + def switch_out(self): + sys.stdin, sys.stderr, sys.stdout = self.saved + self.saved = None + + def throw(self, *args, **kwargs): + # pylint:disable=arguments-differ + if self.saved is None and self._fileobj is not None: + self.switch_in() + Greenlet.throw(self, *args, **kwargs) + + def run(self): + try: + return Greenlet.run(self) + finally: + # Make sure to restore the originals. + self.switch_out() + + +class BackdoorServer(StreamServer): + """ + Provide a backdoor to a program for debugging purposes. + + .. warning:: This backdoor provides no authentication and makes no + attempt to limit what remote users can do. Anyone that + can access the server can take any action that the running + python process can. Thus, while you may bind to any interface, for + security purposes it is recommended that you bind to one + only accessible to the local machine, e.g., + 127.0.0.1/localhost. + + Basic usage:: + + from gevent.backdoor import BackdoorServer + server = BackdoorServer(('127.0.0.1', 5001), + banner="Hello from gevent backdoor!", + locals={'foo': "From defined scope!"}) + server.serve_forever() + + In a another terminal, connect with...:: + + $ telnet 127.0.0.1 5001 + Trying 127.0.0.1... + Connected to 127.0.0.1. + Escape character is '^]'. + Hello from gevent backdoor! + >> print(foo) + From defined scope! + + .. versionchanged:: 1.2a1 + Spawned greenlets are now tracked in a pool and killed when the server + is stopped. + """ + + def __init__(self, listener, locals=None, banner=None, **server_args): + """ + :keyword locals: If given, a dictionary of "builtin" values that will be available + at the top-level. + :keyword banner: If geven, a string that will be printed to each connecting user. + """ + group = Pool(greenlet_class=_Greenlet_stdreplace) # no limit on number + StreamServer.__init__(self, listener, spawn=group, **server_args) + _locals = {'__doc__': None, '__name__': '__console__'} + if locals: + _locals.update(locals) + self.locals = _locals + + self.banner = banner + self.stderr = sys.stderr + + def _create_interactive_locals(self): + # Create and return a *new* locals dictionary based on self.locals, + # and set any new entries in it. (InteractiveConsole does not + # copy its locals value) + _locals = self.locals.copy() + # __builtins__ may either be the __builtin__ module or + # __builtin__.__dict__; in the latter case typing + # locals() at the backdoor prompt spews out lots of + # useless stuff + try: + import __builtin__ + _locals["__builtins__"] = __builtin__ + except ImportError: + import builtins # pylint:disable=import-error + _locals["builtins"] = builtins + _locals['__builtins__'] = builtins + return _locals + + def handle(self, conn, _address): # pylint: disable=method-hidden + """ + Interact with one remote user. + + .. versionchanged:: 1.1b2 Each connection gets its own + ``locals`` dictionary. Previously they were shared in a + potentially unsafe manner. + """ + fobj = conn.makefile(mode="rw") + fobj = _fileobject(conn, fobj, self.stderr) + getcurrent()._fileobj = fobj + + getcurrent().switch_in() + try: + console = InteractiveConsole(self._create_interactive_locals()) + if sys.version_info[:3] >= (3, 6, 0): + # Beginning in 3.6, the console likes to print "now exiting <class>" + # but probably our socket is already closed, so this just causes problems. + console.interact(banner=self.banner, exitmsg='') # pylint:disable=unexpected-keyword-arg + else: + console.interact(banner=self.banner) + except SystemExit: # raised by quit() + if hasattr(sys, 'exc_clear'): # py2 + sys.exc_clear() + finally: + conn.close() + fobj.close() + + +class _fileobject(object): + """ + A file-like object that wraps the result of socket.makefile (composition + instead of inheritance lets us work identically under CPython and PyPy). + + We write directly to the socket, avoiding the buffering that the text-oriented + makefile would want to do (otherwise we'd be at the mercy of waiting on a + flush() to get called for the remote user to see data); this beats putting + the file in binary mode and translating everywhere with a non-default + encoding. + """ + def __init__(self, sock, fobj, stderr): + self._sock = sock + self._fobj = fobj + self.stderr = stderr + + def __getattr__(self, name): + return getattr(self._fobj, name) + + def write(self, data): + if not isinstance(data, bytes): + data = data.encode('utf-8') + self._sock.sendall(data) + + def isatty(self): + return True + + def flush(self): + pass + + def readline(self, *a): + try: + return self._fobj.readline(*a).replace("\r\n", "\n") + except UnicodeError: + # Typically, under python 3, a ^C on the other end + return '' + + +if __name__ == '__main__': + if not sys.argv[1:]: + print('USAGE: %s PORT [banner]' % sys.argv[0]) + else: + BackdoorServer(('127.0.0.1', int(sys.argv[1])), + banner=(sys.argv[2] if len(sys.argv) > 2 else None), + locals={'hello': 'world'}).serve_forever() |