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