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