15cdbf65a6dc8421230e15ae6ad88b6fcd0c0335
[electrum-server.git] / StratumJSONRPCServer.py
1 import jsonrpclib
2 from jsonrpclib import Fault
3 from jsonrpclib.jsonrpc import USE_UNIX_SOCKETS
4 import SimpleXMLRPCServer
5 import SocketServer
6 import socket
7 import logging
8 import os
9 import types
10 import traceback
11 import sys
12 try:
13     import fcntl
14 except ImportError:
15     # For Windows
16     fcntl = None
17
18 import json
19
20 def get_version(request):
21     # must be a dict
22     if 'jsonrpc' in request.keys():
23         return 2.0
24     if 'id' in request.keys():
25         return 1.0
26     return None
27     
28 def validate_request(request):
29     if type(request) is not types.DictType:
30         fault = Fault(
31             -32600, 'Request must be {}, not %s.' % type(request)
32         )
33         return fault
34     rpcid = request.get('id', None)
35     version = get_version(request)
36     if not version:
37         fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid)
38         return fault        
39     request.setdefault('params', [])
40     method = request.get('method', None)
41     params = request.get('params')
42     param_types = (types.ListType, types.DictType, types.TupleType)
43     if not method or type(method) not in types.StringTypes or \
44         type(params) not in param_types:
45         fault = Fault(
46             -32600, 'Invalid request parameters or method.', rpcid=rpcid
47         )
48         return fault
49     return True
50
51 class StratumJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
52
53     def __init__(self, encoding=None):
54         SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
55                                         allow_none=True,
56                                         encoding=encoding)
57
58     def _marshaled_dispatch(self, data, dispatch_method = None):
59         response = None
60         try:
61             request = jsonrpclib.loads(data)
62         except Exception, e:
63             fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e))
64             response = fault.response()
65             return response
66
67         responses = []
68         if type(request) is not types.ListType:
69             request = [ request ]
70
71         for req_entry in request:
72             result = validate_request(req_entry)
73             if type(result) is Fault:
74                 responses.append(result.response())
75                 continue
76             resp_entry = self._marshaled_single_dispatch(req_entry)
77             if resp_entry is not None:
78                 responses.append(resp_entry)
79
80         # poll
81         r = self._marshaled_single_dispatch({'method':'session.poll', 'params':[], 'id':'z' })
82         r = jsonrpclib.loads(r)
83         r = r.get('result')
84         for item in r:
85             responses.append(json.dumps(item))
86             
87         if len(responses) > 1:
88             response = '[%s]' % ','.join(responses)
89         elif len(responses) == 1:
90             response = responses[0]
91         else:
92             response = ''
93
94         return response
95
96     def _marshaled_single_dispatch(self, request):
97         # TODO - Use the multiprocessing and skip the response if
98         # it is a notification
99         # Put in support for custom dispatcher here
100         # (See SimpleXMLRPCServer._marshaled_dispatch)
101         method = request.get('method')
102         params = request.get('params')
103         if params is None: params=[]
104         params = [ self.session_id, request['id'] ] + params
105         print method, params
106         try:
107             response = self._dispatch(method, params)
108         except:
109             exc_type, exc_value, exc_tb = sys.exc_info()
110             fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
111             return fault.response()
112         if 'id' not in request.keys() or request['id'] == None:
113             # It's a notification
114             return None
115
116         try:
117             response = jsonrpclib.dumps(response,
118                                         methodresponse=True,
119                                         rpcid=request['id']
120                                         )
121             return response
122         except:
123             exc_type, exc_value, exc_tb = sys.exc_info()
124             fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
125             return fault.response()
126
127     def _dispatch(self, method, params):
128         func = None
129         try:
130             func = self.funcs[method]
131         except KeyError:
132             if self.instance is not None:
133                 if hasattr(self.instance, '_dispatch'):
134                     return self.instance._dispatch(method, params)
135                 else:
136                     try:
137                         func = SimpleXMLRPCServer.resolve_dotted_attribute(
138                             self.instance,
139                             method,
140                             True
141                             )
142                     except AttributeError:
143                         pass
144         if func is not None:
145             try:
146                 if type(params) is types.ListType:
147                     response = func(*params)
148                 else:
149                     response = func(**params)
150                 return response
151             except TypeError:
152                 return Fault(-32602, 'Invalid parameters.')
153             except:
154                 err_lines = traceback.format_exc().splitlines()
155                 trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
156                 fault = jsonrpclib.Fault(-32603, 'Server error: %s' % 
157                                          trace_string)
158                 return fault
159         else:
160             return Fault(-32601, 'Method %s not supported.' % method)
161
162 class StratumJSONRPCRequestHandler(
163         SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
164     
165     def do_GET(self):
166         if not self.is_rpc_path_valid():
167             self.report_404()
168             return
169         try:
170             print "GET"
171
172             self.server.session_id = None
173             c = self.headers.get('cookie')
174             if c:
175                 if c[0:8]=='SESSION=':
176                     print "found cookie", c[8:]
177                     self.server.session_id = c[8:]
178
179             if self.server.session_id is None:
180                 r = self.server._marshaled_single_dispatch({'method':'session.create', 'params':[], 'id':'z' })
181                 r = jsonrpclib.loads(r)
182                 self.server.session_id = r.get('result')
183                 print "setting cookie", self.server.session_id
184
185             data = json.dumps([])
186             response = self.server._marshaled_dispatch(data)
187             self.send_response(200)
188         except Exception, e:
189             self.send_response(500)
190             err_lines = traceback.format_exc().splitlines()
191             trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
192             fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
193             response = fault.response()
194             print "500", trace_string
195         if response == None:
196             response = ''
197
198         if hasattr(self.server, 'session_id'):
199             if self.server.session_id:
200                 self.send_header("Set-Cookie", "SESSION=%s"%self.server.session_id)
201                 self.session_id = None
202
203         self.send_header("Content-type", "application/json-rpc")
204         self.send_header("Content-length", str(len(response)))
205         self.end_headers()
206         self.wfile.write(response)
207         self.wfile.flush()
208         self.connection.shutdown(1)
209
210
211     def do_POST(self):
212         if not self.is_rpc_path_valid():
213             self.report_404()
214             return
215         try:
216             max_chunk_size = 10*1024*1024
217             size_remaining = int(self.headers["content-length"])
218             L = []
219             while size_remaining:
220                 chunk_size = min(size_remaining, max_chunk_size)
221                 L.append(self.rfile.read(chunk_size))
222                 size_remaining -= len(L[-1])
223             data = ''.join(L)
224
225             self.server.session_id = None
226             c = self.headers.get('cookie')
227             if c:
228                 if c[0:8]=='SESSION=':
229                     print "found cookie", c[8:]
230                     self.server.session_id = c[8:]
231
232             if self.server.session_id is None:
233                 r = self.server._marshaled_single_dispatch({'method':'session.create', 'params':[], 'id':'z' })
234                 r = jsonrpclib.loads(r)
235                 self.server.session_id = r.get('result')
236                 #print "setting cookie", self.server.session_id
237
238             response = self.server._marshaled_dispatch(data)
239             self.send_response(200)
240         except Exception, e:
241             self.send_response(500)
242             err_lines = traceback.format_exc().splitlines()
243             trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
244             fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
245             response = fault.response()
246             print "500", trace_string
247         if response == None:
248             response = ''
249
250         if hasattr(self.server, 'session_id'):
251             if self.server.session_id:
252                 self.send_header("Set-Cookie", "SESSION=%s"%self.server.session_id)
253                 self.session_id = None
254
255         self.send_header("Content-type", "application/json-rpc")
256         self.send_header("Content-length", str(len(response)))
257         self.end_headers()
258         self.wfile.write(response)
259         self.wfile.flush()
260         self.connection.shutdown(1)
261
262
263 class StratumJSONRPCServer(SocketServer.TCPServer, StratumJSONRPCDispatcher):
264
265     allow_reuse_address = True
266
267     def __init__(self, addr, requestHandler=StratumJSONRPCRequestHandler,
268                  logRequests=True, encoding=None, bind_and_activate=True,
269                  address_family=socket.AF_INET):
270         self.logRequests = logRequests
271         StratumJSONRPCDispatcher.__init__(self, encoding)
272         # TCPServer.__init__ has an extra parameter on 2.6+, so
273         # check Python version and decide on how to call it
274         vi = sys.version_info
275         self.address_family = address_family
276         if USE_UNIX_SOCKETS and address_family == socket.AF_UNIX:
277             # Unix sockets can't be bound if they already exist in the
278             # filesystem. The convention of e.g. X11 is to unlink
279             # before binding again.
280             if os.path.exists(addr): 
281                 try:
282                     os.unlink(addr)
283                 except OSError:
284                     logging.warning("Could not unlink socket %s", addr)
285         # if python 2.5 and lower
286         if vi[0] < 3 and vi[1] < 6:
287             SocketServer.TCPServer.__init__(self, addr, requestHandler)
288         else:
289             SocketServer.TCPServer.__init__(self, addr, requestHandler,
290                 bind_and_activate)
291         if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
292             flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
293             flags |= fcntl.FD_CLOEXEC
294             fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
295
296