flame.py 12 KB

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