You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

authproxy.py 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. # Copyright (c) 2011 Jeff Garzik
  2. #
  3. # Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
  4. #
  5. # Copyright (c) 2007 Jan-Klaas Kollhof
  6. #
  7. # This file is part of jsonrpc.
  8. #
  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.
  13. #
  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.
  18. #
  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.
  23. AuthServiceProxy has the following improvements over python-jsonrpc's
  24. ServiceProxy class:
  25. - HTTP connections persist for the life of the AuthServiceProxy object
  26. (if server supports HTTP/1.1)
  27. - sends protocol 'version', per JSON-RPC 1.1
  28. - sends proper, incrementing 'id'
  29. - sends Basic HTTP authentication headers
  30. - parses all JSON numbers that look like floats as Decimal
  31. - uses standard Python json lib
  32. """
  33. try:
  34. import http.client as httplib
  35. except ImportError:
  36. import httplib
  37. import base64
  38. import decimal
  39. import json
  40. import logging
  41. import socket
  42. import time
  43. try:
  44. import urllib.parse as urlparse
  45. except ImportError:
  46. import urlparse
  47. USER_AGENT = "AuthServiceProxy/0.1"
  48. HTTP_TIMEOUT = 30
  49. 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. Exception.__init__(self, errmsg)
  57. self.error = rpc_error
  58. def EncodeDecimal(o):
  59. if isinstance(o, decimal.Decimal):
  60. return str(o)
  61. raise TypeError(repr(o) + " is not JSON serializable")
  62. class AuthServiceProxy(object):
  63. __id_count = 0
  64. # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
  65. def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
  66. self.__service_url = service_url
  67. self._service_name = service_name
  68. self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
  69. self.__url = urlparse.urlparse(service_url)
  70. if self.__url.port is None:
  71. port = 80
  72. else:
  73. port = self.__url.port
  74. (user, passwd) = (self.__url.username, self.__url.password)
  75. try:
  76. user = user.encode('utf8')
  77. except AttributeError:
  78. pass
  79. try:
  80. passwd = passwd.encode('utf8')
  81. except AttributeError:
  82. pass
  83. authpair = user + b':' + passwd
  84. self.__auth_header = b'Basic ' + base64.b64encode(authpair)
  85. if connection:
  86. # Callables re-use the connection of the original proxy
  87. self.__conn = connection
  88. elif self.__url.scheme == 'https':
  89. self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
  90. timeout=timeout)
  91. else:
  92. self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
  93. timeout=timeout)
  94. def __getattr__(self, name):
  95. if name.startswith('__') and name.endswith('__'):
  96. # Python internal stuff
  97. raise AttributeError
  98. if self._service_name is not None:
  99. name = "%s.%s" % (self._service_name, name)
  100. return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
  101. def _request(self, method, path, postdata):
  102. '''
  103. Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
  104. This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
  105. '''
  106. headers = {'Host': self.__url.hostname,
  107. 'User-Agent': USER_AGENT,
  108. 'Authorization': self.__auth_header,
  109. 'Content-type': 'application/json'}
  110. try:
  111. self.__conn.request(method, path, postdata, headers)
  112. return self._get_response()
  113. except httplib.BadStatusLine as e:
  114. if e.line == "''": # if connection was closed, try again
  115. self.__conn.close()
  116. self.__conn.request(method, path, postdata, headers)
  117. return self._get_response()
  118. else:
  119. raise
  120. except (BrokenPipeError,ConnectionResetError):
  121. # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
  122. # ConnectionResetError happens on FreeBSD with Python 3.4
  123. self.__conn.close()
  124. self.__conn.request(method, path, postdata, headers)
  125. return self._get_response()
  126. def __call__(self, *args, **argsn):
  127. AuthServiceProxy.__id_count += 1
  128. log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
  129. json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
  130. if args and argsn:
  131. raise ValueError('Cannot handle both named and positional arguments')
  132. postdata = json.dumps({'version': '1.1',
  133. 'method': self._service_name,
  134. 'params': args or argsn,
  135. 'id': AuthServiceProxy.__id_count}, 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']
  144. def _batch(self, rpc_call_list):
  145. postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
  146. log.debug("--> "+postdata)
  147. return self._request('POST', self.__url.path, postdata.encode('utf-8'))
  148. def _get_response(self):
  149. req_start_time = time.time()
  150. try:
  151. http_response = self.__conn.getresponse()
  152. except socket.timeout as e:
  153. raise JSONRPCException({
  154. 'code': -344,
  155. 'message': '%r RPC took longer than %f seconds. Consider '
  156. 'using larger timeout for calls that take '
  157. 'longer to return.' % (self._service_name,
  158. self.__conn.timeout)})
  159. if http_response is None:
  160. raise JSONRPCException({
  161. 'code': -342, 'message': 'missing HTTP response from server'})
  162. content_type = http_response.getheader('Content-Type')
  163. if content_type != 'application/json':
  164. raise JSONRPCException({
  165. 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
  166. responsedata = http_response.read().decode('utf8')
  167. response = json.loads(responsedata, parse_float=decimal.Decimal)
  168. elapsed = time.time() - req_start_time
  169. if "error" in response and response["error"] is None:
  170. log.debug("<-%s- [%.6f] %s"%(response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
  171. else:
  172. log.debug("<-- [%.6f] %s"%(elapsed,responsedata))
  173. return response