Disable auto-midstate for phoenix/1.50 because a stupid botnet advertises Ufasoft...
[bitcoin:eloipool.git] / jsonrpcserver.py
1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2012  Luke Dashjr <luke-jr+eloipool@utopios.org>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import httpserver
18 import json
19 import logging
20 import networkserver
21 import socket
22 from time import time
23 import traceback
24
25 WithinLongpoll = httpserver.AsyncRequest
26
27 class _SentJSONError(BaseException):
28         def __init__(self, rv):
29                 self.rv = rv
30
31 class JSONRPCHandler(httpserver.HTTPHandler):
32         default_quirks = {
33                 'NELH': None,  # FIXME: identify which clients have a problem with this
34         }
35         
36         LPHeaders = {
37                 'X-Long-Polling': None,
38         }
39         
40         logger = logging.getLogger('JSONRPCHandler')
41         
42         def sendReply(self, status=200, body=b'', headers=None):
43                 headers = dict(headers) if headers else {}
44                 if status == 200:
45                         headers.setdefault('Content-Type', 'application/json')
46                         headers.setdefault('X-Long-Polling', '/LP')
47                         headers.setdefault('X-Roll-NTime', 'expire=120')
48                 elif body and body[0] == 123:  # b'{'
49                         headers.setdefault('Content-Type', 'application/json')
50                 return super().sendReply(status, body, headers)
51         
52         def fmtError(self, reason = '', code = 100):
53                 reason = json.dumps(reason)
54                 reason = r'{"result":null,"id":null,"error":{"name":"JSONRPCError","code":%d,"message":%s}}' % (code, reason)
55                 reason = reason.encode('utf8')
56                 return reason
57         
58         def doError(self, reason = '', code = 100):
59                 reason = self.fmtError(reason, code)
60                 return self.sendReply(500, reason)
61         
62         def checkAuthentication(self, un, pw):
63                 return bool(un)
64         
65         _MidstateNotAdv = (b'phoenix', b'poclbm', b'gminor')
66         def doHeader_user_agent(self, value):
67                 self.reqinfo['UA'] = value
68                 quirks = self.quirks
69                 (UA, v, *x) = value.split(b'/', 1) + [None]
70                 try:
71                         if v[0] == b'v': v = v[1:]
72                         v = tuple(map(int, v.split(b'.'))) + (0,0,0)
73                 except:
74                         pass
75                 if UA in self._MidstateNotAdv:
76                         if UA == b'phoenix':
77                                 if v != (1, 50, 0):
78                                         quirks['midstate'] = None
79                                 if v[0] < 2 and v[1] < 8 and v[2] < 1:
80                                         quirks['NELH'] = None
81                         else:
82                                 quirks['midstate'] = None
83         
84         def doHeader_x_minimum_wait(self, value):
85                 self.reqinfo['MinWait'] = int(value)
86         
87         def doHeader_x_mining_extensions(self, value):
88                 self.extensions = value.decode('ascii').lower().split(' ')
89         
90         def doLongpoll(self, *a):
91                 timeNow = time()
92                 
93                 self._LP = True
94                 self._LPCall = a
95                 if 'NELH' not in self.quirks:
96                         # [NOT No] Early Longpoll Headers
97                         self.sendReply(200, body=None, headers=self.LPHeaders)
98                         self.push(b"1\r\n{\r\n")
99                         self.changeTask(self._chunkedKA, timeNow + 45)
100                 else:
101                         self.changeTask(None)
102                 
103                 waitTime = self.reqinfo.get('MinWait', 15)  # TODO: make default configurable
104                 self.waitTime = waitTime + timeNow
105                 
106                 totfromme = self.LPTrack()
107                 self.server._LPClients[id(self)] = self
108                 self.logger.debug("New LP client; %d total; %d from %s" % (len(self.server._LPClients), totfromme, self.addr[0]))
109                 
110                 raise WithinLongpoll
111         
112         def _chunkedKA(self):
113                 # Keepalive via chunked transfer encoding
114                 self.push(b"1\r\n \r\n")
115                 self.changeTask(self._chunkedKA, time() + 45)
116         
117         def LPTrack(self):
118                 myip = self.addr[0]
119                 if myip not in self.server.LPTracking:
120                         self.server.LPTracking[myip] = 0
121                 self.server.LPTracking[myip] += 1
122                 return self.server.LPTracking[myip]
123         
124         def LPUntrack(self):
125                 self.server.LPTracking[self.addr[0]] -= 1
126         
127         def cleanupLP(self):
128                 # Called when the connection is closed
129                 if not self._LP:
130                         return
131                 self.changeTask(None)
132                 try:
133                         del self.server._LPClients[id(self)]
134                 except KeyError:
135                         pass
136                 self.LPUntrack()
137         
138         def wakeLongpoll(self):
139                 now = time()
140                 if now < self.waitTime:
141                         self.changeTask(self.wakeLongpoll, self.waitTime)
142                         return
143                 else:
144                         self.changeTask(None)
145                 
146                 self.LPUntrack()
147                 
148                 rv = self._doJSON_i(*self._LPCall, longpoll=True)
149                 if 'NELH' not in self.quirks:
150                         rv = rv[1:]  # strip the '{' we already sent
151                         self.push(('%x' % len(rv)).encode('utf8') + b"\r\n" + rv + b"\r\n0\r\n\r\n")
152                 else:
153                         self.sendReply(200, body=rv, headers=self.LPHeaders)
154                 
155                 self.reset_request()
156         
157         def _doJSON_i(self, reqid, method, params, longpoll = False):
158                 try:
159                         rv = getattr(self, method)(*params)
160                 except Exception as e:
161                         self.logger.error(("Error during JSON-RPC call: %s%s\n" % (method, params)) + traceback.format_exc())
162                         efun = self.fmtError if longpoll else self.doError
163                         return efun(r'Service error: %s' % (e,))
164                 if rv is None:
165                         # response was already sent (eg, authentication request)
166                         return
167                 try:
168                         rv.setdefault('submitold', True)
169                 except:
170                         pass
171                 rv = {'id': reqid, 'error': None, 'result': rv}
172                 try:
173                         rv = json.dumps(rv)
174                 except:
175                         efun = self.fmtError if longpoll else self.doError
176                         return efun(r'Error encoding reply in JSON')
177                 rv = rv.encode('utf8')
178                 return rv if longpoll else self.sendReply(200, rv, headers=self._JSONHeaders)
179         
180         def doJSON(self, data, longpoll = False):
181                 # TODO: handle JSON errors
182                 data = data.decode('utf8')
183                 if longpoll and not data:
184                         return self.doLongpoll(1, 'doJSON_getwork', ())
185                 try:
186                         data = json.loads(data)
187                         method = 'doJSON_' + str(data['method']).lower()
188                 except ValueError:
189                         return self.doError(r'Parse error')
190                 except TypeError:
191                         return self.doError(r'Bad call')
192                 if not hasattr(self, method):
193                         return self.doError(r'Procedure not found')
194                 # TODO: handle errors as JSON-RPC
195                 self._JSONHeaders = {}
196                 params = data.setdefault('params', ())
197                 procfun = self._doJSON_i
198                 if longpoll and not params:
199                         procfun = self.doLongpoll
200                 return procfun(data['id'], method, params)
201         
202         def handle_close(self):
203                 self.cleanupLP()
204                 super().handle_close()
205         
206         def handle_request(self):
207                 if not self.Username:
208                         return self.doAuthenticate()
209                 if not self.method in (b'GET', b'POST'):
210                         return self.sendReply(405)
211                 if not self.path in (b'/', b'/LP', b'/LP/'):
212                         return self.sendReply(404)
213                 try:
214                         data = b''.join(self.incoming)
215                         return self.doJSON(data, self.path[:3] == b'/LP')
216                 except socket.error:
217                         raise
218                 except WithinLongpoll:
219                         raise
220                 except:
221                         self.logger.error(traceback.format_exc())
222                         return self.doError('uncaught error')
223         
224         def reset_request(self):
225                 self._LP = False
226                 super().reset_request()
227         
228 setattr(JSONRPCHandler, 'doHeader_user-agent', JSONRPCHandler.doHeader_user_agent);
229 setattr(JSONRPCHandler, 'doHeader_x-minimum-wait', JSONRPCHandler.doHeader_x_minimum_wait);
230 setattr(JSONRPCHandler, 'doHeader_x-mining-extensions', JSONRPCHandler.doHeader_x_mining_extensions);
231
232 JSONRPCListener = networkserver.NetworkListener
233
234 class JSONRPCServer(networkserver.AsyncSocketServer):
235         logger = logging.getLogger('JSONRPCServer')
236         
237         waker = True
238         
239         def __init__(self, *a, **ka):
240                 ka.setdefault('RequestHandlerClass', JSONRPCHandler)
241                 super().__init__(*a, **ka)
242                 
243                 self.SecretUser = None
244                 
245                 self.LPRequest = False
246                 self._LPClients = {}
247                 self._LPWaitTime = time() + 15
248                 
249                 self.LPTracking = {}
250         
251         def pre_schedule(self):
252                 if self.LPRequest == 1:
253                         self._LPsch()
254         
255         def wakeLongpoll(self):
256                 if self.LPRequest:
257                         self.logger.info('Ignoring longpoll attempt while another is waiting')
258                         return
259                 self.LPRequest = 1
260                 self.wakeup()
261         
262         def _LPsch(self):
263                 now = time()
264                 if self._LPWaitTime > now:
265                         delay = self._LPWaitTime - now
266                         self.logger.info('Waiting %.3g seconds to longpoll' % (delay,))
267                         self.schedule(self._actualLP, self._LPWaitTime)
268                         self.LPRequest = 2
269                 else:
270                         self._actualLP()
271         
272         def _actualLP(self):
273                 self.LPRequest = False
274                 C = tuple(self._LPClients.values())
275                 self._LPClients = {}
276                 if not C:
277                         self.logger.info('Nobody to longpoll')
278                         return
279                 OC = len(C)
280                 self.logger.debug("%d clients to wake up..." % (OC,))
281                 
282                 now = time()
283                 
284                 for ic in C:
285                         try:
286                                 ic.wakeLongpoll()
287                         except socket.error:
288                                 OC -= 1
289                                 # Ignore socket errors; let the main event loop take care of them later
290                         except:
291                                 OC -= 1
292                                 self.logger.debug('Error waking longpoll handler:\n' + traceback.format_exc())
293                 
294                 self._LPWaitTime = time()
295                 self.logger.info('Longpoll woke up %d clients in %.3f seconds' % (OC, self._LPWaitTime - now))
296                 self._LPWaitTime += 5  # TODO: make configurable: minimum time between longpolls
297         
298         def TopLPers(self, n = 0x10):
299                 tmp = list(self.LPTracking.keys())
300                 tmp.sort(key=lambda k: self.LPTracking[k])
301                 for jerk in map(lambda k: (k, self.LPTracking[k]), tmp[-n:]):
302                         print(jerk)