""" HTTP gateway: connects the web browser's world of javascript+http and Pyro. Creates a stateless HTTP server that essentially is a proxy for the Pyro objects behind it. It exposes the Pyro objects through a HTTP interface and uses the JSON serializer, so that you can immediately process the response data in the browser. You can start this module as a script from the command line, to easily get a http gateway server running: :command:`python -m Pyro4.utils.httpgateway` or simply: :command:`pyro4-httpgateway` It is also possible to import the 'pyro_app' function and stick that into a WSGI server of your choice, to have more control. The javascript code in the web page of the gateway server works with the same-origin browser policy because it is served by the gateway itself. If you want to access it from scripts in different sites, you have to work around this or embed the gateway app in your site. Non-browser clients that access the http api have no problems. See the `http` example for two of such clients (node.js and python). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from __future__ import print_function import sys import re import cgi import os import uuid import warnings from wsgiref.simple_server import make_server import traceback from Pyro4.util import json # don't import stdlib json directly, we want to use the JSON_MODULE config item from Pyro4.configuration import config from Pyro4 import constants, errors, core, message, util, naming __all__ = ["pyro_app", "main"] _nameserver = None def get_nameserver(hmac=None): global _nameserver if not _nameserver: _nameserver = naming.locateNS(hmac_key=hmac) try: _nameserver.ping() return _nameserver except errors.ConnectionClosedError: _nameserver = None print("Connection with nameserver lost, reconnecting...") return get_nameserver(hmac) def invalid_request(start_response): """Called if invalid http method.""" start_response('405 Method Not Allowed', [('Content-Type', 'text/plain')]) return [b'Error 405: Method Not Allowed'] def not_found(start_response): """Called if Url not found.""" start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'Error 404: Not Found'] def redirect(start_response, target): """Called to do a redirect""" start_response('302 Found', [('Location', target)]) return [] index_page_template = """ Pyro HTTP gateway

Pyro HTTP gateway

Use http+json to talk to Pyro objects. Docs.

Note: performance isn't a key concern here; it is a stateless server. It does a name lookup and uses a new Pyro proxy for each request.

Currently exposed contents of name server on {hostname}:

(Limited to 10 entries, exposed name pattern = '{ns_regex}')

{name_server_contents_list}

Name server examples: (these examples are working if you expose the Pyro.NameServer object)

Echoserver examples: (these examples are working if you expose the test.echoserver object)

Pyro response data (via Ajax):

Call:
   
Response:
   

Pyro version: {pyro_version} — © Irmen de Jong

""" def return_homepage(environ, start_response): try: nameserver = get_nameserver(hmac=pyro_app.hmac_key) except errors.NamingError as x: print("Name server error:", x) start_response('500 Internal Server Error', [('Content-Type', 'text/plain')]) return [b"Cannot connect to the Pyro name server. Is it running? Refresh page to retry."] start_response('200 OK', [('Content-Type', 'text/html')]) nslist = [""] names = sorted(list(nameserver.list(regex=pyro_app.ns_regex).keys())[:10]) with core.batch(nameserver) as nsbatch: for name in names: nsbatch.lookup(name) for name, uri in zip(names, nsbatch()): attributes = "-" try: with core.Proxy(uri) as proxy: proxy._pyroHmacKey = pyro_app.hmac_key proxy._pyroBind() methods = "   ".join(proxy._pyroMethods) or "-" attributes = [ "{attribute}" .format(name=name, attribute=attribute) for attribute in proxy._pyroAttrs ] attributes = "   ".join(attributes) or "-" except errors.PyroError as x: stderr = environ["wsgi.errors"] print("ERROR getting metadata for {0}:".format(uri), file=stderr) traceback.print_exc(file=stderr) methods = "??error:%s??" % str(x) nslist.append( "" .format(name=name, methods=methods, attributes=attributes)) nslist.append("
Namemethodsattributes (zero-param methods)
{name}{methods}{attributes}
") index_page = index_page_template.format(ns_regex=pyro_app.ns_regex, name_server_contents_list="".join(nslist), pyro_version=constants.VERSION, hostname=nameserver._pyroUri.location) return [index_page.encode("utf-8")] def process_pyro_request(environ, path, parameters, start_response): pyro_options = environ.get("HTTP_X_PYRO_OPTIONS", "").split(",") if not path: return return_homepage(environ, start_response) matches = re.match(r"(.+)/(.+)", path) if not matches: return not_found(start_response) object_name, method = matches.groups() if pyro_app.gateway_key: gateway_key = environ.get("HTTP_X_PYRO_GATEWAY_KEY", "") or parameters.get("$key", "") gateway_key = gateway_key.encode("utf-8") if gateway_key != pyro_app.gateway_key: start_response('403 Forbidden', [('Content-Type', 'text/plain')]) return [b"403 Forbidden - incorrect gateway api key"] if "$key" in parameters: del parameters["$key"] if pyro_app.ns_regex and not re.match(pyro_app.ns_regex, object_name): start_response('403 Forbidden', [('Content-Type', 'text/plain')]) return [b"403 Forbidden - access to the requested object has been denied"] try: nameserver = get_nameserver(hmac=pyro_app.hmac_key) uri = nameserver.lookup(object_name) with core.Proxy(uri) as proxy: header_corr_id = environ.get("HTTP_X_PYRO_CORRELATION_ID", "") if header_corr_id: core.current_context.correlation_id = uuid.UUID(header_corr_id) # use the correlation id from the request header else: core.current_context.correlation_id = uuid.uuid4() # set new correlation id proxy._pyroHmacKey = pyro_app.hmac_key proxy._pyroGetMetadata() if "oneway" in pyro_options: proxy._pyroOneway.add(method) if method == "$meta": result = {"methods": tuple(proxy._pyroMethods), "attributes": tuple(proxy._pyroAttrs)} reply = json.dumps(result).encode("utf-8") start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [reply] else: proxy._pyroRawWireResponse = True # we want to access the raw response json if method in proxy._pyroAttrs: # retrieve the attribute assert not parameters, "attribute lookup can't have query parameters" msg = getattr(proxy, method) else: # call the remote method msg = getattr(proxy, method)(**parameters) if msg is None or "oneway" in pyro_options: # was a oneway call, no response available start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [] elif msg.flags & message.FLAGS_EXCEPTION: # got an exception response so send a 500 status start_response('500 Internal Server Error', [('Content-Type', 'application/json; charset=utf-8')]) return [msg.data] else: # normal response start_response('200 OK', [('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(core.current_context.correlation_id))]) return [msg.data] except Exception as x: stderr = environ["wsgi.errors"] print("ERROR handling {0} with params {1}:".format(path, parameters), file=stderr) traceback.print_exc(file=stderr) start_response('500 Internal Server Error', [('Content-Type', 'application/json; charset=utf-8')]) reply = json.dumps(util.SerializerBase.class_to_dict(x)).encode("utf-8") return [reply] def pyro_app(environ, start_response): """ The WSGI app function that is used to process the requests. You can stick this into a wsgi server of your choice, or use the main() method to use the default wsgiref server. """ config.SERIALIZER = "json" # we only talk json through the http proxy config.COMMTIMEOUT = pyro_app.comm_timeout method = environ.get("REQUEST_METHOD") path = environ.get('PATH_INFO', '').lstrip('/') if not path: return redirect(start_response, "/pyro/") if path.startswith("pyro/"): if method in ("GET", "POST"): parameters = singlyfy_parameters(cgi.parse(environ['wsgi.input'], environ)) return process_pyro_request(environ, path[5:], parameters, start_response) else: return invalid_request(start_response) return not_found(start_response) def singlyfy_parameters(parameters): """ Makes a cgi-parsed parameter dictionary into a dict where the values that are just a list of a single value, are converted to just that single value. """ for key, value in parameters.items(): if isinstance(value, (list, tuple)) and len(value) == 1: parameters[key] = value[0] return parameters pyro_app.ns_regex = r"http\." pyro_app.hmac_key = None pyro_app.gateway_key = None pyro_app.comm_timeout = config.COMMTIMEOUT def main(args=None): from optparse import OptionParser parser = OptionParser() parser.add_option("-H", "--host", default="localhost", help="hostname to bind server on (default=%default)") parser.add_option("-p", "--port", type="int", default=8080, help="port to bind server on (default=%default)") parser.add_option("-e", "--expose", default=pyro_app.ns_regex, help="a regex of object names to expose (default=%default)") parser.add_option("-k", "--pyrokey", help="the HMAC key to use to connect with Pyro (deprecated)") parser.add_option("-g", "--gatewaykey", help="the api key to use to connect to the gateway itself") parser.add_option("-t", "--timeout", type="float", default=pyro_app.comm_timeout, help="Pyro timeout value to use (COMMTIMEOUT setting, default=%default)") options, args = parser.parse_args(args) if options.pyrokey or options.gatewaykey: warnings.warn("using -k and/or -g to supply keys on the command line is a security problem " "and is deprecated since Pyro 4.72. See the documentation for an alternative.") if "PYRO_HMAC_KEY" in os.environ: if options.pyrokey: raise SystemExit("error: don't use -k and PYRO_HMAC_KEY at the same time") options.pyrokey = os.environ["PYRO_HMAC_KEY"] if "PYRO_HTTPGATEWAY_KEY" in os.environ: if options.gatewaykey: raise SystemExit("error: don't use -g and PYRO_HTTPGATEWAY_KEY at the same time") options.gatewaykey = os.environ["PYRO_HTTPGATEWAY_KEY"] pyro_app.hmac_key = (options.pyrokey or "").encode("utf-8") pyro_app.gateway_key = (options.gatewaykey or "").encode("utf-8") pyro_app.ns_regex = options.expose pyro_app.comm_timeout = config.COMMTIMEOUT = options.timeout if pyro_app.ns_regex: print("Exposing objects with names matching: ", pyro_app.ns_regex) else: print("Warning: exposing all objects (no expose regex set)") try: ns = get_nameserver(hmac=pyro_app.hmac_key) except errors.PyroError: print("Not yet connected to a name server.") else: print("Connected to name server at: ", ns._pyroUri) server = make_server(options.host, options.port, pyro_app) print("Pyro HTTP gateway running on http://{0}:{1}/pyro/".format(*server.socket.getsockname())) server.serve_forever() server.server_close() return 0 if __name__ == "__main__": sys.exit(main())