Ticket #45: SimpleYAMLRPCServer.py

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

The server

Line 
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 # SimpleYAMLRPCServer.py
4 """Simple YAML-RPC Server.
5
6 This module can be used to create simple YAML-RPC servers
7 by creating a server and either installing functions, a
8 class instance, or by extending the SimpleYAMLRPCServer
9 class.
10
11 It can also be used to handle YAML-RPC requests in a CGI
12 environment using CGIYAMLRPCRequestHandler.
13
14 A list of possible usage patterns follows:
15
16 1. Install functions:
17
18 server = SimpleYAMLRPCServer(("localhost", 8000))
19 server.register_function(pow)
20 server.register_function(lambda x,y: x+y, 'add')
21 server.serve_forever()
22
23 2. Install an instance:
24
25 class 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
39 server = SimpleYAMLRPCServer(("localhost", 8000))
40 server.register_introspection_functions()
41 server.register_instance(MyFuncs())
42 server.serve_forever()
43
44 3. Install an instance with custom dispatch method:
45
46 class 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
70 server = SimpleXMLRPCServer(("localhost", 8000))
71 server.register_introspection_functions()
72 server.register_instance(Math())
73 server.serve_forever()
74
75 4. Subclass SimpleYAMLRPCServer:
76
77 class 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
92 server = MathServer(("localhost", 8000))
93 server.serve_forever()
94
95 5. CGI script:
96
97 server = CGIYAMLRPCRequestHandler()
98 server.register_function(pow)
99 server.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
110 import xmlrpclib
111 from xmlrpclib import Fault
112 import SocketServer
113 import BaseHTTPServer
114 import sys
115 import os
116
117 import SimpleXMLRPCServer
118 import yaml
119 from yamlrpcbasic import *
120
121 import traceback
122
123 class 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
206 class 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    
246 class 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
263 class 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    
319 if __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