Ticket #45: SimpleYAMLRPCServer.py

File SimpleYAMLRPCServer.py, 11.6 KB (added by pkmurphy at postmaster dot co dot uk, 7 years ago)

The server

Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# SimpleYAMLRPCServer.py
4"""Simple YAML-RPC Server.
5
6This module can be used to create simple YAML-RPC servers
7by creating a server and either installing functions, a
8class instance, or by extending the SimpleYAMLRPCServer
9class.
10
11It can also be used to handle YAML-RPC requests in a CGI
12environment using CGIYAMLRPCRequestHandler.
13
14A list of possible usage patterns follows:
15
161. Install functions:
17
18server = SimpleYAMLRPCServer(("localhost", 8000))
19server.register_function(pow)
20server.register_function(lambda x,y: x+y, 'add')
21server.serve_forever()
22
232. Install an instance:
24
25class MyFuncs:
26    def __init__(self):
27        # make all of the string functions available through
28        # string.func_name
29        import string
30        self.string = string
31    def _listMethods(self):
32        # implement this method so that system.listMethods
33        # knows to advertise the strings methods
34        return list_public_methods(self) + \
35                ['string.' + method for method in list_public_methods(self.string)]
36    def pow(self, x, y): return pow(x, y)
37    def add(self, x, y) : return x + y
38
39server = SimpleYAMLRPCServer(("localhost", 8000))
40server.register_introspection_functions()
41server.register_instance(MyFuncs())
42server.serve_forever()
43
443. Install an instance with custom dispatch method:
45
46class Math:
47    def _listMethods(self):
48        # this method must be present for system.listMethods
49        # to work
50        return ['add', 'pow']
51    def _methodHelp(self, method):
52        # this method must be present for system.methodHelp
53        # to work
54        if method == 'add':
55            return "add(2,3) => 5"
56        elif method == 'pow':
57            return "pow(x, y[, z]) => number"
58        else:
59            # By convention, return empty
60            # string if no help is available
61            return ""
62    def _dispatch(self, method, params):
63        if method == 'pow':
64            return pow(*params)
65        elif method == 'add':
66            return params[0] + params[1]
67        else:
68            raise 'bad method'
69
70server = SimpleXMLRPCServer(("localhost", 8000))
71server.register_introspection_functions()
72server.register_instance(Math())
73server.serve_forever()
74
754. Subclass SimpleYAMLRPCServer:
76
77class MathServer(SimpleYAMLRPCServer):
78    def _dispatch(self, method, params):
79        try:
80            # We are forcing the 'export_' prefix on methods that are
81            # callable through YAML-RPC to prevent potential security
82            # problems
83            func = getattr(self, 'export_' + method)
84        except AttributeError:
85            raise Exception('method "%s" is not supported' % method)
86        else:
87            return func(*params)
88
89    def export_add(self, x, y):
90        return x + y
91
92server = MathServer(("localhost", 8000))
93server.serve_forever()
94
955. CGI script:
96
97server = CGIYAMLRPCRequestHandler()
98server.register_function(pow)
99server.handle_request()
100"""
101
102# This implementation is based on SimpleJSONRPCServer, which was
103# was converted from SimpleXMLRPCServer by David McNab
104# (david@rebirthing.co.nz). Current implementation written by Peter
105# Murphy.
106
107# Original SimpleXMLRPCServer module was written by Brian
108# Quinlan (brian@sweetapp.com), Based on code written by Fredrik Lundh.
109
110import xmlrpclib
111from xmlrpclib import Fault
112import SocketServer
113import BaseHTTPServer
114import sys
115import os
116
117import SimpleXMLRPCServer
118import yaml
119from yamlrpcbasic import *
120
121import traceback
122
123class SimpleYAMLRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
124    """ Mix-in class that dispatches YAML-RPC requests. Based on
125        SimpleXMLRPCDispatcher, but overrides marshaled_dispatch for
126        YAML-RPC.
127
128        This class is used to register YAML-RPC method handlers and then to
129        dispatch them. There should never be any reason to instantiate this
130        class directly.
131    """
132
133    def __init__(self, safe_loader = True, json_compat = True):
134        """ The parameters are:
135            safe_loader: the default is True: only allows "simple" YAML data to
136            be loaded - sequences, dictionaries and most immutable types. If
137            this parameter is False, then arbitrary YAML data can be loaded.
138            This can be a security risk. (Only set this parameter to False with
139            trusted clients!)
140           
141            json_compat: if True, all complex objects are sent to YAML-RPC
142            clients as dictionaries; no special tagging printed out. If False,
143            YAML-RPC responses are delivered with the tag '!yamlrpcret'. The
144            default is True.
145        """
146        self.json_compat = json_compat;
147        self.loadfunc(safe_loader);
148        SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, True, "utf-8");
149
150    def loadfunc(self, safe_loader):
151        """ Sets the load function. """
152        self.safe_loader = safe_loader;
153        if self.safe_loader:
154            self._loadfunc = yaml.safe_load;
155        else:
156            self._loadfunc = yaml.load;
157
158    def _marshaled_dispatch(self, data, dispatch_method = None):
159        """ Dispatches a YAML-RPC method from marshalled (YAML) data.
160   
161            YAML-RPC methods are dispatched from the marshalled (YAML) data
162            using the _dispatch method and the result is returned as
163            marshalled data. For backwards compatibility, a dispatch
164            function can be provided as an argument (see comment in
165            SimpleYAMLRPCRequestHandler.do_POST) but overriding the
166            existing method through subclassing is the prefered means of
167            changing method dispatch behavior.
168       
169            The "data" part can be one of the following:
170            (a) An object with tag '!yamlrpccall'.
171            (b) A dictionary/YAML map (with the same keys). This is to allow
172            backward compatibilty with JSON-RPC.
173       
174        """
175        rawreq = self._loadfunc(data)
176        id = getAttrWay(rawreq, "id", None);
177        method = getAttrWay(rawreq, "method", None);
178        params = getAttrWay(rawreq, "params", []);
179        OurResponseObj = YAMLRPCReturn(id, "1.0", None, None);
180       
181   
182        # We generate a response.
183        try:
184            if dispatch_method is not None:
185                response = dispatch_method(method, params)
186            else:
187                response = self._dispatch(method, params)
188            OurResponseObj.result = response
189#        except Fault, fault:
190            #response = xmlrpclib.dumps(fault)
191 #           OurResponseObj.error = YAMLRPCError("YAMLRPCError", 666, repr(response), None);
192        except:
193            OurResponseObj.error = YAMLRPCError("YAMLRPCError", 667, 
194                "%s:%s" % (sys.exc_type, sys.exc_value), None);
195
196# The following code is for JSON compatibility.
197
198        if self.json_compat:
199            OurResponseObj = OurResponseObj.__dict__;
200            OurError = OurResponseObj["error"];
201            if OurError != None:
202                OurResponseObj["error"] = OurError.__dict__;
203        return yaml.dump(OurResponseObj);
204   
205
206class SimpleYAMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
207    """Simple YAML-RPC request handler class.
208
209    Handles all HTTP POST requests and attempts to decode them as
210    XML-RPC requests.
211    """
212    def do_POST(self):
213        """Handles the HTTP POST request.
214   
215        Attempts to interpret all HTTP POST requests as YAML-RPC calls,
216        which are forwarded to the server's _dispatch method for handling.
217        """
218        try:
219            # get arguments
220            data = self.rfile.read(int(self.headers["content-length"]))
221            # In previous versions of SimpleXMLRPCServer, _dispatch
222            # could be overridden in this class, instead of in
223            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
224            # check to see if a subclass implements _dispatch and dispatch
225            # using that method if present.
226            response = self.server._marshaled_dispatch(
227                    data, getattr(self, '_dispatch', None)
228                )
229        except: # This should only happen if the module is buggy
230            # internal error, report as HTTP server error
231            self.send_response(500)
232            self.end_headers()
233            raise;
234        else:
235            # got a valid XML RPC response
236            self.send_response(200)
237            self.send_header("Content-type", "text/yaml")
238            self.send_header("Content-length", str(len(response)))
239            self.end_headers()
240            self.wfile.write(response)
241   
242            # shut down the connection
243            self.wfile.flush()
244            self.connection.shutdown(1)
245   
246class SimpleYAMLRPCServer(SocketServer.TCPServer,
247                         SimpleYAMLRPCDispatcher):
248    """Simple YAML-RPC server.
249
250    Simple YAML-RPC server that allows functions and a single instance
251    to be installed to handle requests. The default implementation
252    attempts to dispatch YAML-RPC calls to the functions or instance
253    installed in the server. Override the _dispatch method inhereted
254    from SimpleYAMLRPCDispatcher to change this behavior.
255    """
256    def __init__(self, addr, requestHandler=SimpleYAMLRPCRequestHandler,
257                 logRequests=1, safe_loader = True, json_compat = True):
258        self.logRequests = logRequests
259
260        SimpleYAMLRPCDispatcher.__init__(self, safe_loader, json_compat)
261        SocketServer.TCPServer.__init__(self, addr, requestHandler)
262
263class CGIYAMLRPCRequestHandler(SimpleYAMLRPCDispatcher):
264    """Simple handler for YAML-RPC data passed through CGI."""
265   
266    def __init__(self, safe_loader = True, json_compat = True):
267        SimpleYAMLRPCDispatcher.__init__(safe_loader, json_compat)
268   
269    def handle_get(self):
270        """Handle a single HTTP GET request.
271   
272        Default implementation indicates an error because
273        XML-RPC uses the POST method.
274        """
275   
276        code = 400
277        message, explain = \
278                 BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
279   
280        response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
281            {
282             'code' : code,
283             'message' : message,
284             'explain' : explain
285            }
286        print 'Status: %d %s' % (code, message)
287        print 'Content-Type: text/html'
288        print 'Content-Length: %d' % len(response)
289        print
290        sys.stdout.write(response)
291   
292    def handle_request(self, request_text = None):
293        """Handle a single YAML-RPC request passed through a CGI post method.
294   
295        If no YAML data is given then it is read from stdin. The resulting
296        YAML-RPC response is printed to stdout along with the correct HTTP
297        headers.
298        """
299        if request_text is None and \
300            os.environ.get('REQUEST_METHOD', None) == 'GET':
301            self.handle_get()
302        else:
303            # POST data is normally available through stdin
304            if request_text is None:
305                request_text = sys.stdin.read()
306   
307            self.handle_yamlrpc(request_text)
308   
309    def handle_yamlrpc(self, request_text):
310        """Handle a single YAML-RPC request"""
311   
312        response = self._marshaled_dispatch(request_text)
313   
314        print 'Content-Type: text/yaml'
315        print 'Content-Length: %d' % len(response)
316        print
317        sys.stdout.write(response)
318   
319if __name__ == '__main__':
320    server = SimpleYAMLRPCServer(("localhost", 8002), safe_loader = False, json_compat = True)
321    server.register_function(pow)
322    server.register_function(lambda x,y: x+y, 'add');
323    server.register_function(lambda x: x, u"echo");
324    server.register_introspection_functions();
325    server.serve_forever()
326
327