flame.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. """
  2. Pyro FLAME: Foreign Location Automatic Module Exposer.
  3. Easy but potentially very dangerous way of exposing remote modules and builtins.
  4. Flame requires the pickle serializer to be used.
  5. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net).
  6. """
  7. import sys
  8. import types
  9. import code
  10. import os
  11. import stat
  12. import Pyro4.core
  13. import Pyro4.util
  14. import Pyro4.constants
  15. import Pyro4.errors
  16. try:
  17. import importlib
  18. except ImportError:
  19. importlib = None
  20. try:
  21. import builtins
  22. except ImportError:
  23. import __builtin__ as builtins
  24. try:
  25. from cStringIO import StringIO
  26. except ImportError:
  27. from io import StringIO
  28. __all__ = ["connect", "start", "createModule", "Flame"]
  29. # Exec is a statement in Py2, a function in Py3
  30. # Workaround as written by Ned Batchelder on his blog.
  31. if sys.version_info > (3, 0):
  32. def exec_function(source, filename, global_map):
  33. source = fixExecSourceNewlines(source)
  34. exec(compile(source, filename, "exec"), global_map)
  35. else:
  36. # OK, this is pretty gross. In Py2, exec was a statement, but that will
  37. # be a syntax error if we try to put it in a Py3 file, even if it isn't
  38. # executed. So hide it inside an evaluated string literal instead.
  39. eval(compile("""\
  40. def exec_function(source, filename, global_map):
  41. source=fixExecSourceNewlines(source)
  42. exec compile(source, filename, "exec") in global_map
  43. """, "<exec_function>", "exec"))
  44. def fixExecSourceNewlines(source):
  45. if sys.version_info < (2, 7) or sys.version_info[:2] in ((3, 0), (3, 1)):
  46. # for python versions prior to 2.7 (and 3.0/3.1), compile is kinda picky.
  47. # it needs unix type newlines and a trailing newline to work correctly.
  48. source = source.replace("\r\n", "\n")
  49. source = source.rstrip() + "\n"
  50. # remove trailing whitespace that might cause IndentationErrors
  51. source = source.rstrip()
  52. return source
  53. class FlameModule(object):
  54. """Proxy to a remote module."""
  55. def __init__(self, flameserver, module):
  56. # store a proxy to the flameserver regardless of autoproxy setting
  57. self.flameserver = Pyro4.core.Proxy(flameserver._pyroDaemon.uriFor(flameserver))
  58. self.module = module
  59. def __getattr__(self, item):
  60. if item in ("__getnewargs__", "__getnewargs_ex__", "__getinitargs__"):
  61. raise AttributeError(item)
  62. return Pyro4.core._RemoteMethod(self.__invoke, "%s.%s" % (self.module, item), 0)
  63. def __getstate__(self):
  64. return self.__dict__
  65. def __setstate__(self, args):
  66. self.__dict__ = args
  67. def __invoke(self, module, args, kwargs):
  68. return self.flameserver.invokeModule(module, args, kwargs)
  69. def __enter__(self):
  70. return self
  71. def __exit__(self, exc_type, exc_value, traceback):
  72. self.flameserver._pyroRelease()
  73. def __repr__(self):
  74. return "<%s.%s at 0x%x; module '%s' at %s>" % (self.__class__.__module__, self.__class__.__name__,
  75. id(self), self.module, self.flameserver._pyroUri.location)
  76. class FlameBuiltin(object):
  77. """Proxy to a remote builtin function."""
  78. def __init__(self, flameserver, builtin):
  79. # store a proxy to the flameserver regardless of autoproxy setting
  80. self.flameserver = Pyro4.core.Proxy(flameserver._pyroDaemon.uriFor(flameserver))
  81. self.builtin = builtin
  82. def __call__(self, *args, **kwargs):
  83. return self.flameserver.invokeBuiltin(self.builtin, args, kwargs)
  84. def __enter__(self):
  85. return self
  86. def __exit__(self, exc_type, exc_value, traceback):
  87. self.flameserver._pyroRelease()
  88. def __repr__(self):
  89. return "<%s.%s at 0x%x; builtin '%s' at %s>" % (self.__class__.__module__, self.__class__.__name__,
  90. id(self), self.builtin, self.flameserver._pyroUri.location)
  91. class RemoteInteractiveConsole(object):
  92. """Proxy to a remote interactive console."""
  93. class LineSendingConsole(code.InteractiveConsole):
  94. """makes sure the lines are sent to the remote console"""
  95. def __init__(self, remoteconsole):
  96. code.InteractiveConsole.__init__(self, filename="<remoteconsole>")
  97. self.remoteconsole = remoteconsole
  98. def push(self, line):
  99. output, more = self.remoteconsole.push_and_get_output(line)
  100. if output:
  101. sys.stdout.write(output)
  102. return more
  103. def __init__(self, remoteconsoleuri):
  104. # store a proxy to the console regardless of autoproxy setting
  105. self.remoteconsole = Pyro4.core.Proxy(remoteconsoleuri)
  106. def interact(self):
  107. console = self.LineSendingConsole(self.remoteconsole)
  108. console.interact(banner=self.remoteconsole.get_banner())
  109. print("(Remote session ended)")
  110. def close(self):
  111. self.remoteconsole.terminate()
  112. self.remoteconsole._pyroRelease()
  113. def terminate(self):
  114. self.close()
  115. def __repr__(self):
  116. return "<%s.%s at 0x%x; for %s>" % (self.__class__.__module__, self.__class__.__name__,
  117. id(self), self.remoteconsole._pyroUri.location)
  118. def __enter__(self):
  119. return self
  120. def __exit__(self, exc_type, exc_value, traceback):
  121. self.close()
  122. @Pyro4.expose
  123. class InteractiveConsole(code.InteractiveConsole):
  124. """Interactive console wrapper that saves output written to stdout so it can be returned as value"""
  125. def push_and_get_output(self, line):
  126. output, more = "", False
  127. stdout_save = sys.stdout
  128. try:
  129. sys.stdout = StringIO()
  130. more = self.push(line)
  131. output = sys.stdout.getvalue()
  132. sys.stdout.close()
  133. finally:
  134. sys.stdout = stdout_save
  135. return output, more
  136. def get_banner(self):
  137. return self.banner # custom banner string, set by Pyro daemon
  138. def write(self, data):
  139. sys.stdout.write(data) # stdout instead of stderr
  140. def terminate(self):
  141. self._pyroDaemon.unregister(self)
  142. self.resetbuffer()
  143. @Pyro4.expose
  144. class Flame(object):
  145. """
  146. The actual FLAME server logic.
  147. Usually created by using :py:meth:`Pyro4.core.Daemon.startFlame`.
  148. Be *very* cautious before starting this: it allows the clients full access to everything on your system.
  149. """
  150. def __init__(self):
  151. if set(Pyro4.config.SERIALIZERS_ACCEPTED) != {"pickle"}:
  152. raise RuntimeError("flame requires the pickle serializer exclusively")
  153. def module(self, name):
  154. """import a module on the server given by the module name and returns a proxy to it"""
  155. if importlib:
  156. importlib.import_module(name)
  157. else:
  158. __import__(name)
  159. return FlameModule(self, name)
  160. def builtin(self, name):
  161. """returns a proxy to the given builtin on the server"""
  162. return FlameBuiltin(self, name)
  163. def execute(self, code):
  164. """execute a piece of code"""
  165. exec_function(code, "<remote-code>", globals())
  166. def evaluate(self, expression):
  167. """evaluate an expression and return its result"""
  168. return eval(expression)
  169. def sendmodule(self, modulename, modulesource):
  170. """
  171. Send the source of a module to the server and make the server load it.
  172. Note that you still have to actually ``import`` it on the server to access it.
  173. Sending a module again will replace the previous one with the new.
  174. """
  175. createModule(modulename, modulesource)
  176. def getmodule(self, modulename):
  177. """obtain the source code from a module on the server"""
  178. import inspect
  179. module = __import__(modulename, globals={}, locals={})
  180. return inspect.getsource(module)
  181. def sendfile(self, filename, filedata):
  182. """store a new file on the server"""
  183. with open(filename, "wb") as targetfile:
  184. os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR) # readable/writable by owner only
  185. targetfile.write(filedata)
  186. def getfile(self, filename):
  187. """read any accessible file from the server"""
  188. with open(filename, "rb") as diskfile:
  189. return diskfile.read()
  190. def console(self):
  191. """get a proxy for a remote interactive console session"""
  192. console = InteractiveConsole(filename="<remoteconsole>")
  193. uri = self._pyroDaemon.register(console)
  194. console.banner = "Python %s on %s\n(Remote console on %s)" % (sys.version, sys.platform, uri.location)
  195. return RemoteInteractiveConsole(uri)
  196. @Pyro4.expose
  197. def invokeBuiltin(self, builtin, args, kwargs):
  198. return getattr(builtins, builtin)(*args, **kwargs)
  199. @Pyro4.expose
  200. def invokeModule(self, dottedname, args, kwargs):
  201. # dottedname is something like "os.path.walk" so strip off the module name
  202. modulename, dottedname = dottedname.split('.', 1)
  203. module = sys.modules[modulename]
  204. # Look up the actual method to call.
  205. # Because Flame already opens all doors, security wise, we allow ourselves to
  206. # look up a dotted name via object traversal. The security implication of that
  207. # is overshadowed by the security implications of enabling Flame in the first place.
  208. # We also don't check for access to 'private' methods. Same reasons.
  209. method = module
  210. for attr in dottedname.split('.'):
  211. method = getattr(method, attr)
  212. return method(*args, **kwargs)
  213. def createModule(name, source, filename="<dynamic-module>", namespace=None):
  214. """
  215. Utility function to create a new module with the given name (dotted notation allowed), directly from the source string.
  216. Adds it to sys.modules, and returns the new module object.
  217. If you provide a namespace dict (such as ``globals()``), it will import the module into that namespace too.
  218. """
  219. path = ""
  220. components = name.split('.')
  221. module = types.ModuleType("pyro-flame-module-context")
  222. for component in components:
  223. # build the module hierarchy.
  224. path += '.' + component
  225. real_path = path[1:]
  226. if real_path in sys.modules:
  227. # use already loaded modules instead of overwriting them
  228. module = sys.modules[real_path]
  229. else:
  230. setattr(module, component, types.ModuleType(real_path))
  231. module = getattr(module, component)
  232. sys.modules[real_path] = module
  233. exec_function(source, filename, module.__dict__)
  234. if namespace is not None:
  235. namespace[components[0]] = __import__(name)
  236. return module
  237. def start(daemon):
  238. """
  239. Create and register a Flame server in the given daemon.
  240. Be *very* cautious before starting this: it allows the clients full access to everything on your system.
  241. """
  242. if Pyro4.config.FLAME_ENABLED:
  243. if set(Pyro4.config.SERIALIZERS_ACCEPTED) != {"pickle"}:
  244. raise Pyro4.errors.SerializeError("Flame requires the pickle serializer exclusively")
  245. return daemon.register(Flame(), Pyro4.constants.FLAME_NAME)
  246. else:
  247. raise Pyro4.errors.SecurityError("Flame is disabled in the server configuration")
  248. def connect(location):
  249. """
  250. Connect to a Flame server on the given location, for instance localhost:9999 or ./u:unixsock
  251. This is just a convenience function to creates an appropriate Pyro proxy.
  252. """
  253. if Pyro4.config.SERIALIZER != "pickle":
  254. raise Pyro4.errors.SerializeError("Flame requires the pickle serializer")
  255. proxy = Pyro4.core.Proxy("PYRO:%s@%s" % (Pyro4.constants.FLAME_NAME, location))
  256. proxy._pyroBind()
  257. return proxy