1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2012 Luke Dashjr <luke-jr+eloipool@utopios.org>
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.
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.
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/>.
25 WithinLongpoll = httpserver.AsyncRequest
27 class JSONRPCHandler(httpserver.HTTPHandler):
29 'NELH': None, # FIXME: identify which clients have a problem with this
33 'X-Long-Polling': None,
36 logger = logging.getLogger('JSONRPCHandler')
38 def sendReply(self, status=200, body=b'', headers=None):
39 headers = dict(headers) if headers else {}
41 headers.setdefault('Content-Type', 'application/json')
42 headers.setdefault('X-Long-Polling', '/LP')
43 headers.setdefault('X-Roll-NTime', 'expire=120')
44 elif body and body[0] == 123: # b'{'
45 headers.setdefault('Content-Type', 'application/json')
46 return super().sendReply(status, body, headers)
48 def doError(self, reason = '', code = 100):
49 reason = json.dumps(reason)
50 reason = r'{"result":null,"id":null,"error":{"name":"JSONRPCError","code":%d,"message":%s}}' % (code, reason)
51 return self.sendReply(500, reason.encode('utf8'))
53 def checkAuthentication(self, un, pw):
56 def doHeader_user_agent(self, value):
57 self.reqinfo['UA'] = value
60 if value[:9] == b'phoenix/v':
61 v = tuple(map(int, value[9:].split(b'.')))
62 if v[0] < 2 and v[1] < 8 and v[2] < 1:
68 def doHeader_x_minimum_wait(self, value):
69 self.reqinfo['MinWait'] = int(value)
71 def doHeader_x_mining_extensions(self, value):
72 self.extensions = value.decode('ascii').lower().split(' ')
78 if 'NELH' not in self.quirks:
79 # [NOT No] Early Longpoll Headers
80 self.sendReply(200, body=None, headers=self.LPHeaders)
81 self.push(b"1\r\n{\r\n")
82 self.changeTask(self._chunkedKA, timeNow + 45)
86 waitTime = self.reqinfo.get('MinWait', 15) # TODO: make default configurable
87 self.waitTime = waitTime + timeNow
89 totfromme = self.LPTrack()
90 self.server._LPClients[id(self)] = self
91 self.logger.debug("New LP client; %d total; %d from %s" % (len(self.server._LPClients), totfromme, self.addr[0]))
96 # Keepalive via chunked transfer encoding
97 self.push(b"1\r\n \r\n")
98 self.changeTask(self._chunkedKA, time() + 45)
102 if myip not in self.server.LPTracking:
103 self.server.LPTracking[myip] = 0
104 self.server.LPTracking[myip] += 1
105 return self.server.LPTracking[myip]
108 self.server.LPTracking[self.addr[0]] -= 1
111 # Called when the connection is closed
114 self.changeTask(None)
116 del self.server._LPClients[id(self)]
121 def wakeLongpoll(self):
123 if now < self.waitTime:
124 self.changeTask(self.wakeLongpoll, self.waitTime)
127 self.changeTask(None)
131 rv = self.doJSON_getwork()
132 rv['submitold'] = True
133 rv = {'id': 1, 'error': None, 'result': rv}
135 rv = rv.encode('utf8')
136 if 'NELH' not in self.quirks:
137 rv = rv[1:] # strip the '{' we already sent
138 self.push(('%x' % len(rv)).encode('utf8') + b"\r\n" + rv + b"\r\n0\r\n\r\n")
140 self.sendReply(200, body=rv, headers=self.LPHeaders)
144 def doJSON(self, data):
145 # TODO: handle JSON errors
146 data = data.decode('utf8')
148 data = json.loads(data)
149 method = 'doJSON_' + str(data['method']).lower()
151 return self.doError(r'Parse error')
153 return self.doError(r'Bad call')
154 if not hasattr(self, method):
155 return self.doError(r'Procedure not found')
156 # TODO: handle errors as JSON-RPC
157 self._JSONHeaders = {}
158 params = data.setdefault('params', ())
160 rv = getattr(self, method)(*tuple(data['params']))
161 except Exception as e:
162 self.logger.error(("Error during JSON-RPC call: %s%s\n" % (method, params)) + traceback.format_exc())
163 return self.doError(r'Service error: %s' % (e,))
165 # response was already sent (eg, authentication request)
167 rv = {'id': data['id'], 'error': None, 'result': rv}
171 return self.doError(r'Error encoding reply in JSON')
172 rv = rv.encode('utf8')
173 return self.sendReply(200, rv, headers=self._JSONHeaders)
175 def handle_close(self):
177 super().handle_close()
179 def handle_request(self):
180 if not self.Username:
181 return self.doAuthenticate()
182 if not self.method in (b'GET', b'POST'):
183 return self.sendReply(405)
184 if not self.path in (b'/', b'/LP', b'/LP/'):
185 return self.sendReply(404)
187 if self.path[:3] == b'/LP':
188 return self.doLongpoll()
189 data = b''.join(self.incoming)
190 return self.doJSON(data)
193 except WithinLongpoll:
196 self.logger.error(traceback.format_exc())
197 return self.doError('uncaught error')
199 def reset_request(self):
201 super().reset_request()
203 setattr(JSONRPCHandler, 'doHeader_user-agent', JSONRPCHandler.doHeader_user_agent);
204 setattr(JSONRPCHandler, 'doHeader_x-minimum-wait', JSONRPCHandler.doHeader_x_minimum_wait);
205 setattr(JSONRPCHandler, 'doHeader_x-mining-extensions', JSONRPCHandler.doHeader_x_mining_extensions);
207 JSONRPCListener = networkserver.NetworkListener
209 class JSONRPCServer(networkserver.AsyncSocketServer):
210 logger = logging.getLogger('JSONRPCServer')
214 def __init__(self, *a, **ka):
215 ka.setdefault('RequestHandlerClass', JSONRPCHandler)
216 super().__init__(*a, **ka)
218 self.SecretUser = None
220 self.LPRequest = False
222 self._LPWaitTime = time() + 15
226 def pre_schedule(self):
227 if self.LPRequest == 1:
230 def wakeLongpoll(self):
232 self.logger.info('Ignoring longpoll attempt while another is waiting')
239 if self._LPWaitTime > now:
240 delay = self._LPWaitTime - now
241 self.logger.info('Waiting %.3g seconds to longpoll' % (delay,))
242 self.schedule(self._actualLP, self._LPWaitTime)
248 self.LPRequest = False
249 C = tuple(self._LPClients.values())
252 self.logger.info('Nobody to longpoll')
255 self.logger.debug("%d clients to wake up..." % (OC,))
264 # Ignore socket errors; let the main event loop take care of them later
267 self.logger.debug('Error waking longpoll handler:\n' + traceback.format_exc())
269 self._LPWaitTime = time()
270 self.logger.info('Longpoll woke up %d clients in %.3f seconds' % (OC, self._LPWaitTime - now))
271 self._LPWaitTime += 5 # TODO: make configurable: minimum time between longpolls
273 def TopLPers(self, n = 0x10):
274 tmp = list(self.LPTracking.keys())
275 tmp.sort(key=lambda k: self.LPTracking[k])
276 for jerk in map(lambda k: (k, self.LPTracking[k]), tmp[-n:]):