Bugfix: bitcoin/script: Correctly interpret P2SH addresses, and reject anything unrec...
[bitcoin:eloipool.git] / jsonrpcserver.py
1 from base64 import b64decode
2 from binascii import a2b_hex, b2a_hex
3 from datetime import datetime
4 from email.utils import formatdate
5 import json
6 import logging
7 import select
8 import socket
9 import socketserver
10 from time import mktime
11 import traceback
12 from util import RejectedShare, swap32
13
14 # TODO: keepalive/close
15 _CheckForDupesHACK = {}
16 class JSONRPCHandler(socketserver.StreamRequestHandler):
17         HTTPStatus = {
18                 200: 'OK',
19                 401: 'Unauthorized',
20                 404: 'Not Found',
21                 405: 'Method Not Allowed',
22                 500: 'Internal Server Error',
23         }
24         
25         logger = logging.getLogger('JSONRPCHandler')
26         
27         def sendReply(self, status=200, body=b'', headers=None):
28                 wfile = self.wfile
29                 buf = "HTTP/1.1 %d %s\n" % (status, self.HTTPStatus.get(status, 'Eligius'))
30                 headers = dict(headers) if headers else {}
31                 headers['Date'] = formatdate(timeval=mktime(datetime.now().timetuple()), localtime=False, usegmt=True)
32                 if body is None:
33                         headers.setdefault('Transfer-Encoding', 'chunked')
34                         body = b''
35                 else:
36                         headers['Content-Length'] = len(body)
37                 if status == 200:
38                         headers.setdefault('Content-Type', 'application/json')
39                         #headers.setdefault('X-Long-Polling', '/LP')
40                 for k, v in headers.items():
41                         if v is None: continue
42                         buf += "%s: %s\n" % (k, v)
43                 buf += "\n"
44                 buf = buf.encode('utf8')
45                 buf += body
46                 wfile.write(buf)
47         
48         def doError(self, reason = ''):
49                 return self.sendReply(500, reason.encode('utf8'))
50         
51         def doHeader_authorization(self, value):
52                 value = value.split(b' ')
53                 if len(value) != 2 or value[0] != b'Basic':
54                         return self.doError('Bad Authorization header')
55                 value = b64decode(value[1])
56                 value = value.split(b':')[0]
57                 self.Username = value
58         
59         def doHeader_content_length(self, value):
60                 self.CL = int(value)
61         
62         def doAuthenticate(self):
63                 self.sendReply(401, headers={'WWW-Authenticate': 'Basic realm="Eligius"'})
64         
65         def doLongpoll(self):
66                 pass # TODO
67         
68         def doJSON(self, data):
69                 # TODO: handle JSON errors
70                 data = data.decode('utf8')
71                 data = json.loads(data)
72                 method = 'doJSON_' + str(data['method']).lower()
73                 if not hasattr(self, method):
74                         return self.doError('No such method')
75                 # TODO: handle errors as JSON-RPC
76                 self._JSONHeaders = {}
77                 rv = getattr(self, method)(*tuple(data['params']))
78                 rv = {'id': data['id'], 'error': None, 'result': rv}
79                 rv = json.dumps(rv)
80                 rv = rv.encode('utf8')
81                 return self.sendReply(200, rv, headers=self._JSONHeaders)
82         
83         getwork_rv_template = {
84                 'data': '000000800000000000000000000000000000000000000000000000000000000000000000000000000000000080020000',
85                 'target': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000',
86                 'hash1': '00000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000010000',
87         }
88         def doJSON_getwork(self, data=None):
89                 if not data is None:
90                         return self.doJSON_submitwork(data)
91                 rv = dict(self.getwork_rv_template)
92                 hdr = self.server.getBlockHeader(self.Username)
93                 
94                 # FIXME: this assumption breaks with noncerange or rollntime
95                 global _CheckForDupesHACK
96                 if hdr in _CheckForDupesHACK:
97                         raise self.server.RaiseRedFlags(RuntimeError('issuing duplicate work'))
98                 _CheckForDupesHACK[hdr] = None
99                 
100                 data = b2a_hex(swap32(hdr)).decode('utf8') + rv['data']
101                 # TODO: endian shuffle etc
102                 rv['data'] = data
103                 # TODO: rv['midstate'] = 
104                 return rv
105         
106         def doJSON_submitwork(self, data):
107                 data = swap32(a2b_hex(data))[:80]
108                 share = {
109                         'data': data,
110                         'username': self.Username,
111                 }
112                 try:
113                         self.server.receiveShare(share)
114                 except RejectedShare as rej:
115                         self._JSONHeaders['X-Reject-Reason'] = str(rej)
116                         return False
117                 return True
118         
119         def handle_i(self):
120                 # TODO: handle socket errors
121                 rfile = self.rfile
122                 data = rfile.readline().strip()
123                 data = data.split(b' ')
124                 if not data[0] in (b'GET', b'POST'):
125                         return self.sendReply(405)
126                 path = data[1]
127                 if not path in (b'/', b'/LP'):
128                         return self.sendReply(404)
129                 self.CL = None
130                 self.Username = None
131                 while True:
132                         data = rfile.readline().strip()
133                         if not data:
134                                 break
135                         data = tuple(map(lambda a: a.strip(), data.split(b':', 1)))
136                         method = 'doHeader_' + data[0].decode('ascii').lower()
137                         if hasattr(self, method):
138                                 getattr(self, method)(data[1])
139                 if not self.Username:
140                         return self.doAuthenticate()
141                 data = rfile.read(self.CL) if self.CL else None
142                 try:
143                         if path == b'/LP':
144                                 return self.doLongpoll()
145                         return self.doJSON(data)
146                 except:
147                         self.logger.error(traceback.format_exc())
148                         return self.doError('uncaught error')
149         
150         def handle(self):
151                 try:
152                         while True:
153                                 self.handle_i()
154                 except socket.error:
155                         pass
156         
157 setattr(JSONRPCHandler, 'doHeader_content-length', JSONRPCHandler.doHeader_content_length);
158
159 class JSONRPCServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
160         allow_reuse_address = True
161         daemon_threads = True
162         
163         def __init__(self, server_address, RequestHandlerClass=JSONRPCHandler, *a, **k):
164                 super().__init__(server_address, RequestHandlerClass, *a, **k)
165         
166         def serve_forever(self, *a, **k):
167                 while True:
168                         try:
169                                 super().serve_forever(*a, **k)
170                         except select.error:
171                                 pass