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