1 # Copyright (c) 2011 Jeff Garzik
3 # Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
5 # Copyright (c) 2007 Jan-Klaas Kollhof
7 # This file is part of jsonrpc.
9 # jsonrpc is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU Lesser General Public License as published by
11 # the Free Software Foundation; either version 2.1 of the License, or
12 # (at your option) any later version.
14 # This software is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Lesser General Public License for more details.
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with this software; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 """HTTP proxy for opening RPC connection to bitcoind.
24 AuthServiceProxy has the following improvements over python-jsonrpc's
27 - HTTP connections persist for the life of the AuthServiceProxy object
28 (if server supports HTTP/1.1)
29 - sends protocol 'version', per JSON-RPC 1.1
30 - sends proper, incrementing 'id'
31 - sends Basic HTTP authentication headers
32 - parses all JSON numbers that look like floats as Decimal
33 - uses standard Python json lib
46 USER_AGENT
= "AuthServiceProxy/0.1"
48 log
= logging
.getLogger("BitcoinRPC")
50 class JSONRPCException(Exception):
51 def __init__(self
, rpc_error
):
53 errmsg
= '%(message)s (%(code)i)' % rpc_error
54 except (KeyError, TypeError):
56 super().__init
__(errmsg
)
57 self
.error
= rpc_error
61 if isinstance(o
, decimal
.Decimal
):
63 raise TypeError(repr(o
) + " is not JSON serializable")
65 class AuthServiceProxy():
68 # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
69 def __init__(self
, service_url
, service_name
=None, timeout
=HTTP_TIMEOUT
, connection
=None, ensure_ascii
=True):
70 self
.__service
_url
= service_url
71 self
._service
_name
= service_name
72 self
.ensure_ascii
= ensure_ascii
# can be toggled on the fly by tests
73 self
.__url
= urllib
.parse
.urlparse(service_url
)
74 port
= 80 if self
.__url
.port
is None else self
.__url
.port
75 user
= None if self
.__url
.username
is None else self
.__url
.username
.encode('utf8')
76 passwd
= None if self
.__url
.password
is None else self
.__url
.password
.encode('utf8')
77 authpair
= user
+ b
':' + passwd
78 self
.__auth
_header
= b
'Basic ' + base64
.b64encode(authpair
)
81 # Callables re-use the connection of the original proxy
82 self
.__conn
= connection
83 elif self
.__url
.scheme
== 'https':
84 self
.__conn
= http
.client
.HTTPSConnection(self
.__url
.hostname
, port
, timeout
=timeout
)
86 self
.__conn
= http
.client
.HTTPConnection(self
.__url
.hostname
, port
, timeout
=timeout
)
88 def __getattr__(self
, name
):
89 if name
.startswith('__') and name
.endswith('__'):
90 # Python internal stuff
92 if self
._service
_name
is not None:
93 name
= "%s.%s" % (self
._service
_name
, name
)
94 return AuthServiceProxy(self
.__service
_url
, name
, connection
=self
.__conn
)
96 def _request(self
, method
, path
, postdata
):
98 Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
99 This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
101 headers
= {'Host': self
.__url
.hostname
,
102 'User-Agent': USER_AGENT
,
103 'Authorization': self
.__auth
_header
,
104 'Content-type': 'application/json'}
106 self
.__conn
.request(method
, path
, postdata
, headers
)
107 return self
._get
_response
()
108 except http
.client
.BadStatusLine
as e
:
109 if e
.line
== "''": # if connection was closed, try again
111 self
.__conn
.request(method
, path
, postdata
, headers
)
112 return self
._get
_response
()
115 except (BrokenPipeError
, ConnectionResetError
):
116 # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
117 # ConnectionResetError happens on FreeBSD with Python 3.4
119 self
.__conn
.request(method
, path
, postdata
, headers
)
120 return self
._get
_response
()
122 def get_request(self
, *args
, **argsn
):
123 AuthServiceProxy
.__id
_count
+= 1
125 log
.debug("-%s-> %s %s" % (AuthServiceProxy
.__id
_count
, self
._service
_name
,
126 json
.dumps(args
, default
=EncodeDecimal
, ensure_ascii
=self
.ensure_ascii
)))
128 raise ValueError('Cannot handle both named and positional arguments')
129 return {'version': '1.1',
130 'method': self
._service
_name
,
131 'params': args
or argsn
,
132 'id': AuthServiceProxy
.__id
_count
}
134 def __call__(self
, *args
, **argsn
):
135 postdata
= json
.dumps(self
.get_request(*args
, **argsn
), default
=EncodeDecimal
, ensure_ascii
=self
.ensure_ascii
)
136 response
= self
._request
('POST', self
.__url
.path
, postdata
.encode('utf-8'))
137 if response
['error'] is not None:
138 raise JSONRPCException(response
['error'])
139 elif 'result' not in response
:
140 raise JSONRPCException({
141 'code': -343, 'message': 'missing JSON-RPC result'})
143 return response
['result']
145 def batch(self
, rpc_call_list
):
146 postdata
= json
.dumps(list(rpc_call_list
), default
=EncodeDecimal
, ensure_ascii
=self
.ensure_ascii
)
147 log
.debug("--> " + postdata
)
148 return self
._request
('POST', self
.__url
.path
, postdata
.encode('utf-8'))
150 def _get_response(self
):
151 req_start_time
= time
.time()
153 http_response
= self
.__conn
.getresponse()
154 except socket
.timeout
as e
:
155 raise JSONRPCException({
157 'message': '%r RPC took longer than %f seconds. Consider '
158 'using larger timeout for calls that take '
159 'longer to return.' % (self
._service
_name
,
160 self
.__conn
.timeout
)})
161 if http_response
is None:
162 raise JSONRPCException({
163 'code': -342, 'message': 'missing HTTP response from server'})
165 content_type
= http_response
.getheader('Content-Type')
166 if content_type
!= 'application/json':
167 raise JSONRPCException({
168 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response
.status
, http_response
.reason
)})
170 responsedata
= http_response
.read().decode('utf8')
171 response
= json
.loads(responsedata
, parse_float
=decimal
.Decimal
)
172 elapsed
= time
.time() - req_start_time
173 if "error" in response
and response
["error"] is None:
174 log
.debug("<-%s- [%.6f] %s" % (response
["id"], elapsed
, json
.dumps(response
["result"], default
=EncodeDecimal
, ensure_ascii
=self
.ensure_ascii
)))
176 log
.debug("<-- [%.6f] %s" % (elapsed
, responsedata
))
179 def __truediv__(self
, relative_uri
):
180 return AuthServiceProxy("{}/{}".format(self
.__service
_url
, relative_uri
), self
._service
_name
, connection
=self
.__conn
)