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 45try: 46 import urllib.parse as urlparse 47except ImportError: 48 import urlparse 49 50USER_AGENT = "AuthServiceProxy/0.1" 51 52HTTP_TIMEOUT = 30 53 54log = logging.getLogger("BitcoinRPC") 55 56class JSONRPCException(Exception): 57 def __init__(self, rpc_error): 58 parent_args = [] 59 try: 60 parent_args.append(rpc_error['message']) 61 except: 62 pass 63 Exception.__init__(self, *parent_args) 64 self.error = rpc_error 65 self.code = rpc_error['code'] if 'code' in rpc_error else None 66 self.message = rpc_error['message'] if 'message' in rpc_error else None 67 68 def __str__(self): 69 return '%d: %s' % (self.code, self.message) 70 71 def __repr__(self): 72 return '<%s \'%s\'>' % (self.__class__.__name__, self) 73 74 75def EncodeDecimal(o): 76 if isinstance(o, decimal.Decimal): 77 return float(round(o, 8)) 78 raise TypeError(repr(o) + " is not JSON serializable") 79 80class AuthServiceProxy(object): 81 __id_count = 0 82 83 def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None): 84 self.__service_url = service_url 85 self.__service_name = service_name 86 self.__url = urlparse.urlparse(service_url) 87 if self.__url.port is None: 88 port = 80 89 else: 90 port = self.__url.port 91 (user, passwd) = (self.__url.username, self.__url.password) 92 try: 93 user = user.encode('utf8') 94 except AttributeError: 95 pass 96 try: 97 passwd = passwd.encode('utf8') 98 except AttributeError: 99 pass 100 authpair = user + b':' + passwd 101 self.__auth_header = b'Basic ' + base64.b64encode(authpair) 102 103 self.__timeout = timeout 104 105 if connection: 106 # Callables re-use the connection of the original proxy 107 self.__conn = connection 108 elif self.__url.scheme == 'https': 109 self.__conn = httplib.HTTPSConnection(self.__url.hostname, port, 110 timeout=timeout) 111 else: 112 self.__conn = httplib.HTTPConnection(self.__url.hostname, port, 113 timeout=timeout) 114 115 def __getattr__(self, name): 116 if name.startswith('__') and name.endswith('__'): 117 # Python internal stuff 118 raise AttributeError 119 if self.__service_name is not None: 120 name = "%s.%s" % (self.__service_name, name) 121 return AuthServiceProxy(self.__service_url, name, self.__timeout, self.__conn) 122 123 def __call__(self, *args): 124 AuthServiceProxy.__id_count += 1 125 126 log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self.__service_name, 127 json.dumps(args, default=EncodeDecimal))) 128 postdata = json.dumps({'version': '1.1', 129 'method': self.__service_name, 130 'params': args, 131 'id': AuthServiceProxy.__id_count}, default=EncodeDecimal) 132 self.__conn.request('POST', self.__url.path, postdata, 133 {'Host': self.__url.hostname, 134 'User-Agent': USER_AGENT, 135 'Authorization': self.__auth_header, 136 'Content-type': 'application/json'}) 137 self.__conn.sock.settimeout(self.__timeout) 138 139 response = self._get_response() 140 if response.get('error') is not None: 141 raise JSONRPCException(response['error']) 142 elif 'result' not in response: 143 raise JSONRPCException({ 144 'code': -343, 'message': 'missing JSON-RPC result'}) 145 146 return response['result'] 147 148 def batch_(self, rpc_calls): 149 """Batch RPC call. 150 Pass array of arrays: [ [ "method", params... ], ... ] 151 Returns array of results. 152 """ 153 batch_data = [] 154 for rpc_call in rpc_calls: 155 AuthServiceProxy.__id_count += 1 156 m = rpc_call.pop(0) 157 batch_data.append({"jsonrpc":"2.0", "method":m, "params":rpc_call, "id":AuthServiceProxy.__id_count}) 158 159 postdata = json.dumps(batch_data, default=EncodeDecimal) 160 log.debug("--> "+postdata) 161 self.__conn.request('POST', self.__url.path, postdata, 162 {'Host': self.__url.hostname, 163 'User-Agent': USER_AGENT, 164 'Authorization': self.__auth_header, 165 'Content-type': 'application/json'}) 166 results = [] 167 responses = self._get_response() 168 for response in responses: 169 if response['error'] is not None: 170 raise JSONRPCException(response['error']) 171 elif 'result' not in response: 172 raise JSONRPCException({ 173 'code': -343, 'message': 'missing JSON-RPC result'}) 174 else: 175 results.append(response['result']) 176 return results 177 178 def _get_response(self): 179 http_response = self.__conn.getresponse() 180 if http_response is None: 181 raise JSONRPCException({ 182 'code': -342, 'message': 'missing HTTP response from server'}) 183 184 content_type = http_response.getheader('Content-Type') 185 if content_type != 'application/json': 186 raise JSONRPCException({ 187 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) 188 189 responsedata = http_response.read().decode('utf8') 190 response = json.loads(responsedata, parse_float=decimal.Decimal) 191 if "error" in response and response["error"] is None: 192 log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal))) 193 else: 194 log.debug("<-- "+responsedata) 195 return response 196