Setting fcntl to None if not importable, adding tests module, updating README to...
[invirt/packages/python-jsonrpclib.git] / jsonrpclib / SimpleJSONRPCServer.py
1 import jsonrpclib
2 from jsonrpclib import Fault
3 import SimpleXMLRPCServer
4 import SocketServer
5 import types
6 import traceback
7 import sys
8 try:
9     import fcntl
10 except ImportError:
11     # For Windows
12     fcntl = None
13
14 def get_version(request):
15     # must be a dict
16     if 'jsonrpc' in request.keys():
17         return 2.0
18     if 'id' in request.keys():
19         return 1.0
20     return None
21     
22 def validate_request(request):
23     if type(request) is not types.DictType:
24         fault = Fault(
25             -32600, 'Request must be {}, not %s.' % type(request)
26         )
27         return fault
28     rpcid = request.get('id', None)
29     version = get_version(request)
30     if not version:
31         fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid)
32         return fault        
33     request.setdefault('params', [])
34     method = request.get('method', None)
35     params = request.get('params')
36     param_types = (types.ListType, types.DictType, types.TupleType)
37     if not method or type(method) not in types.StringTypes or \
38         type(params) not in param_types:
39         fault = Fault(
40             -32600, 'Invalid request parameters or method.', rpcid=rpcid
41         )
42         return fault
43     return True
44
45 class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
46
47     def __init__(self, encoding=None):
48         SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
49                                         allow_none=True,
50                                         encoding=encoding)
51
52     def _marshaled_dispatch(self, data, dispatch_method = None):
53         response = None
54         try:
55             request = jsonrpclib.loads(data)
56         except Exception, e:
57             fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e))
58             response = fault.response()
59             return response
60         if not request:
61             fault = Fault(-32600, 'Request invalid -- no request data.')
62             return fault.response()
63         if type(request) is types.ListType:
64             # This SHOULD be a batch, by spec
65             responses = []
66             for req_entry in request:
67                 result = validate_request(req_entry)
68                 if type(result) is Fault:
69                     responses.append(result.response())
70                     continue
71                 resp_entry = self._marshaled_single_dispatch(req_entry)
72                 if resp_entry is not None:
73                     responses.append(resp_entry)
74             if len(responses) > 0:
75                 response = '[%s]' % ','.join(responses)
76             else:
77                 response = ''
78         else:    
79             result = validate_request(request)
80             if type(result) is Fault:
81                 return result.response()
82             response = self._marshaled_single_dispatch(request)
83         return response
84
85     def _marshaled_single_dispatch(self, request):
86         # TODO - Use the multiprocessing and skip the response if
87         # it is a notification
88         # Put in support for custom dispatcher here
89         # (See SimpleXMLRPCServer._marshaled_dispatch)
90         method = request.get('method')
91         params = request.get('params')
92         try:
93             response = self._dispatch(method, params)
94         except:
95             exc_type, exc_value, exc_tb = sys.exc_info()
96             fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
97             return fault.response()
98         if 'id' not in request.keys() or request['id'] == None:
99             # It's a notification
100             return None
101         try:
102             response = jsonrpclib.dumps(response,
103                                         methodresponse=True,
104                                         rpcid=request['id']
105                                         )
106             return response
107         except:
108             exc_type, exc_value, exc_tb = sys.exc_info()
109             fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
110             return fault.response()
111
112     def _dispatch(self, method, params):
113         func = None
114         try:
115             func = self.funcs[method]
116         except KeyError:
117             if self.instance is not None:
118                 if hasattr(self.instance, '_dispatch'):
119                     return self.instance._dispatch(method, params)
120                 else:
121                     try:
122                         func = SimpleXMLRPCServer.resolve_dotted_attribute(
123                             self.instance,
124                             method,
125                             True
126                             )
127                     except AttributeError:
128                         pass
129         if func is not None:
130             try:
131                 if type(params) is types.ListType:
132                     response = func(*params)
133                 else:
134                     response = func(**params)
135                 return response
136             except TypeError:
137                 return Fault(-32602, 'Invalid parameters.')
138             except:
139                 err_lines = traceback.format_exc().splitlines()
140                 trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
141                 fault = jsonrpclib.Fault(-32603, 'Server error: %s' % 
142                                          trace_string)
143                 return fault
144         else:
145             return Fault(-32601, 'Method %s not supported.' % method)
146
147 class SimpleJSONRPCRequestHandler(
148         SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
149     
150     def do_POST(self):
151         if not self.is_rpc_path_valid():
152             self.report_404()
153             return
154         try:
155             max_chunk_size = 10*1024*1024
156             size_remaining = int(self.headers["content-length"])
157             L = []
158             while size_remaining:
159                 chunk_size = min(size_remaining, max_chunk_size)
160                 L.append(self.rfile.read(chunk_size))
161                 size_remaining -= len(L[-1])
162             data = ''.join(L)
163             response = self.server._marshaled_dispatch(data)
164             self.send_response(200)
165         except Exception, e:
166             self.send_response(500)
167             err_lines = traceback.format_exc().splitlines()
168             trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
169             fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
170             response = fault.response()
171         if response == None:
172             response = ''
173         self.send_header("Content-type", "application/json-rpc")
174         self.send_header("Content-length", str(len(response)))
175         self.end_headers()
176         self.wfile.write(response)
177         self.wfile.flush()
178         self.connection.shutdown(1)
179
180 class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher):
181
182     allow_reuse_address = True
183
184     def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
185                  logRequests=True, encoding=None, bind_and_activate=True):
186         self.logRequests = logRequests
187         SimpleJSONRPCDispatcher.__init__(self, encoding)
188         # TCPServer.__init__ has an extra parameter on 2.6+, so
189         # check Python version and decide on how to call it
190         vi = sys.version_info
191         # if python 2.5 and lower
192         if vi[0] < 3 and vi[1] < 6:
193             SocketServer.TCPServer.__init__(self, addr, requestHandler)
194         else:
195             SocketServer.TCPServer.__init__(self, addr, requestHandler,
196                 bind_and_activate)
197         if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
198             flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
199             flags |= fcntl.FD_CLOEXEC
200             fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
201
202 class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):
203
204     def __init__(self, encoding=None):
205         SimpleJSONRPCDispatcher.__init__(self, encoding)
206
207     def handle_jsonrpc(self, request_text):
208         response = self._marshaled_dispatch(request_text)
209         print 'Content-Type: application/json-rpc'
210         print 'Content-Length: %d' % len(response)
211         print
212         sys.stdout.write(response)
213
214     handle_xmlrpc = handle_jsonrpc