3fe44329628771440372367b41d68fe1d351c83d
[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             self.server.session_id = None
171             c = self.headers.get('cookie')
172             if c:
173                 if c[0:8]=='SESSION=':
174                     #print "found cookie", c[8:]
175                     self.server.session_id = c[8:]
176
177             if self.server.session_id is None:
178                 r = self.server._marshaled_single_dispatch({'method':'session.create', 'params':[], 'id':'z' })
179                 r = jsonrpclib.loads(r)
180                 self.server.session_id = r.get('result')
181                 #print "setting cookie", self.server.session_id
182
183             data = json.dumps([])
184             response = self.server._marshaled_dispatch(data)
185             self.send_response(200)
186         except Exception, e:
187             self.send_response(500)
188             err_lines = traceback.format_exc().splitlines()
189             trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
190             fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
191             response = fault.response()
192             print "500", trace_string
193         if response == None:
194             response = ''
195
196         if hasattr(self.server, 'session_id'):
197             if self.server.session_id:
198                 self.send_header("Set-Cookie", "SESSION=%s"%self.server.session_id)
199                 self.session_id = None
200
201         self.send_header("Content-type", "application/json-rpc")
202         self.send_header("Content-length", str(len(response)))
203         self.end_headers()
204         self.wfile.write(response)
205         self.wfile.flush()
206         self.connection.shutdown(1)
207
208
209     def do_POST(self):
210         if not self.is_rpc_path_valid():
211             self.report_404()
212             return
213         try:
214             max_chunk_size = 10*1024*1024
215             size_remaining = int(self.headers["content-length"])
216             L = []
217             while size_remaining:
218                 chunk_size = min(size_remaining, max_chunk_size)
219                 L.append(self.rfile.read(chunk_size))
220                 size_remaining -= len(L[-1])
221             data = ''.join(L)
222
223             self.server.session_id = None
224             c = self.headers.get('cookie')
225             if c:
226                 if c[0:8]=='SESSION=':
227                     #print "found cookie", c[8:]
228                     self.server.session_id = c[8:]
229
230             if self.server.session_id is None:
231                 r = self.server._marshaled_single_dispatch({'method':'session.create', 'params':[], 'id':'z' })
232                 r = jsonrpclib.loads(r)
233                 self.server.session_id = r.get('result')
234                 #print "setting cookie", self.server.session_id
235
236             response = self.server._marshaled_dispatch(data)
237             self.send_response(200)
238         except Exception, e:
239             self.send_response(500)
240             err_lines = traceback.format_exc().splitlines()
241             trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
242             fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
243             response = fault.response()
244             print "500", trace_string
245         if response == None:
246             response = ''
247
248         if hasattr(self.server, 'session_id'):
249             if self.server.session_id:
250                 self.send_header("Set-Cookie", "SESSION=%s"%self.server.session_id)
251                 self.session_id = None
252
253         self.send_header("Content-type", "application/json-rpc")
254         self.send_header("Content-length", str(len(response)))
255         self.end_headers()
256         self.wfile.write(response)
257         self.wfile.flush()
258         self.connection.shutdown(1)
259
260
261 class StratumJSONRPCServer(SocketServer.TCPServer, StratumJSONRPCDispatcher):
262
263     allow_reuse_address = True
264
265     def __init__(self, addr, requestHandler=StratumJSONRPCRequestHandler,
266                  logRequests=False, encoding=None, bind_and_activate=True,
267                  address_family=socket.AF_INET):
268         self.logRequests = logRequests
269         StratumJSONRPCDispatcher.__init__(self, encoding)
270         # TCPServer.__init__ has an extra parameter on 2.6+, so
271         # check Python version and decide on how to call it
272         vi = sys.version_info
273         self.address_family = address_family
274         if USE_UNIX_SOCKETS and address_family == socket.AF_UNIX:
275             # Unix sockets can't be bound if they already exist in the
276             # filesystem. The convention of e.g. X11 is to unlink
277             # before binding again.
278             if os.path.exists(addr): 
279                 try:
280                     os.unlink(addr)
281                 except OSError:
282                     logging.warning("Could not unlink socket %s", addr)
283         # if python 2.5 and lower
284         if vi[0] < 3 and vi[1] < 6:
285             SocketServer.TCPServer.__init__(self, addr, requestHandler)
286         else:
287             SocketServer.TCPServer.__init__(self, addr, requestHandler,
288                 bind_and_activate)
289         if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
290             flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
291             flags |= fcntl.FD_CLOEXEC
292             fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
293
294