[tests] Don't subclass from object for Python 3
[bitcoinplatinum.git] / test / functional / test_framework / authproxy.py
blobbd3a3b3fabebe0f2d306733d83f827a1810b9b91
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
25 ServiceProxy class:
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
34 """
36 import base64
37 import decimal
38 import http.client
39 import json
40 import logging
41 import socket
42 import time
43 import urllib.parse
45 HTTP_TIMEOUT = 30
46 USER_AGENT = "AuthServiceProxy/0.1"
48 log = logging.getLogger("BitcoinRPC")
50 class JSONRPCException(Exception):
51 def __init__(self, rpc_error):
52 try:
53 errmsg = '%(message)s (%(code)i)' % rpc_error
54 except (KeyError, TypeError):
55 errmsg = ''
56 super().__init__(errmsg)
57 self.error = rpc_error
60 def EncodeDecimal(o):
61 if isinstance(o, decimal.Decimal):
62 return str(o)
63 raise TypeError(repr(o) + " is not JSON serializable")
65 class AuthServiceProxy():
66 __id_count = 0
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)
80 if connection:
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)
85 else:
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
91 raise AttributeError
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):
97 '''
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'}
105 try:
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
110 self.__conn.close()
111 self.__conn.request(method, path, postdata, headers)
112 return self._get_response()
113 else:
114 raise
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
118 self.__conn.close()
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)))
127 if args and argsn:
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'})
142 else:
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()
152 try:
153 http_response = self.__conn.getresponse()
154 except socket.timeout as e:
155 raise JSONRPCException({
156 'code': -344,
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)))
175 else:
176 log.debug("<-- [%.6f] %s" % (elapsed, responsedata))
177 return response
179 def __truediv__(self, relative_uri):
180 return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn)