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.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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. try:
  43. import urllib.parse as urlparse
  44. except ImportError:
  45. import urlparse
  46. USER_AGENT = "AuthServiceProxy/0.1"
  47. HTTP_TIMEOUT = 30
  48. log = logging.getLogger("BitcoinRPC")
  49. class JSONRPCException(Exception):
  50. def __init__(self, rpc_error):
  51. try:
  52. errmsg = '%(message)s (%(code)i)' % rpc_error
  53. except (KeyError, TypeError):
  54. errmsg = ''
  55. Exception.__init__(self, errmsg)
  56. self.error = rpc_error
  57. def EncodeDecimal(o):
  58. if isinstance(o, decimal.Decimal):
  59. return str(o)
  60. raise TypeError(repr(o) + " is not JSON serializable")
  61. class AuthServiceProxy(object):
  62. __id_count = 0
  63. # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
  64. def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
  65. self.__service_url = service_url
  66. self._service_name = service_name
  67. self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
  68. self.__url = urlparse.urlparse(service_url)
  69. if self.__url.port is None:
  70. port = 80
  71. else:
  72. port = self.__url.port
  73. (user, passwd) = (self.__url.username, self.__url.password)
  74. try:
  75. user = user.encode('utf8')
  76. except AttributeError:
  77. pass
  78. try:
  79. passwd = passwd.encode('utf8')
  80. except AttributeError:
  81. pass
  82. authpair = user + b':' + passwd
  83. self.__auth_header = b'Basic ' + base64.b64encode(authpair)
  84. if connection:
  85. # Callables re-use the connection of the original proxy
  86. self.__conn = connection
  87. elif self.__url.scheme == 'https':
  88. self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
  89. timeout=timeout)
  90. else:
  91. self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
  92. timeout=timeout)
  93. def __getattr__(self, name):
  94. if name.startswith('__') and name.endswith('__'):
  95. # Python internal stuff
  96. raise AttributeError
  97. if self._service_name is not None:
  98. name = "%s.%s" % (self._service_name, name)
  99. return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
  100. def _request(self, method, path, postdata):
  101. '''
  102. Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
  103. This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
  104. '''
  105. headers = {'Host': self.__url.hostname,
  106. 'User-Agent': USER_AGENT,
  107. 'Authorization': self.__auth_header,
  108. 'Content-type': 'application/json'}
  109. try:
  110. self.__conn.request(method, path, postdata, headers)
  111. return self._get_response()
  112. except httplib.BadStatusLine as e:
  113. if e.line == "''": # if connection was closed, try again
  114. self.__conn.close()
  115. self.__conn.request(method, path, postdata, headers)
  116. return self._get_response()
  117. else:
  118. raise
  119. except (BrokenPipeError,ConnectionResetError):
  120. # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
  121. # ConnectionResetError happens on FreeBSD with Python 3.4
  122. self.__conn.close()
  123. self.__conn.request(method, path, postdata, headers)
  124. return self._get_response()
  125. def __call__(self, *args, **argsn):
  126. AuthServiceProxy.__id_count += 1
  127. log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
  128. json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
  129. if args and argsn:
  130. raise ValueError('Cannot handle both named and positional arguments')
  131. postdata = json.dumps({'version': '1.1',
  132. 'method': self._service_name,
  133. 'params': args or argsn,
  134. 'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
  135. response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
  136. if response['error'] is not None:
  137. raise JSONRPCException(response['error'])
  138. elif 'result' not in response:
  139. raise JSONRPCException({
  140. 'code': -343, 'message': 'missing JSON-RPC result'})
  141. else:
  142. return response['result']
  143. def _batch(self, rpc_call_list):
  144. postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
  145. log.debug("--> "+postdata)
  146. return self._request('POST', self.__url.path, postdata.encode('utf-8'))
  147. def _get_response(self):
  148. try:
  149. http_response = self.__conn.getresponse()
  150. except socket.timeout as e:
  151. raise JSONRPCException({
  152. 'code': -344,
  153. 'message': '%r RPC took longer than %f seconds. Consider '
  154. 'using larger timeout for calls that take '
  155. 'longer to return.' % (self._service_name,
  156. self.__conn.timeout)})
  157. if http_response is None:
  158. raise JSONRPCException({
  159. 'code': -342, 'message': 'missing HTTP response from server'})
  160. content_type = http_response.getheader('Content-Type')
  161. if content_type != 'application/json':
  162. raise JSONRPCException({
  163. 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
  164. responsedata = http_response.read().decode('utf8')
  165. response = json.loads(responsedata, parse_float=decimal.Decimal)
  166. if "error" in response and response["error"] is None:
  167. log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
  168. else:
  169. log.debug("<-- "+responsedata)
  170. return response