import http.client as httplib
import base64
import decimal
import json
import logging
USER_AGENT = "AuthServiceProxy/0.1"
HTTP_TIMEOUT = 120
log = logging.getLogger("BitcoinRPC")
class JSONRPCException(Exception):
def __init__(self, rpc_error):
Exception.__init__(self)
self.error = rpc_error
def __str__(self):
return f"{self.error['code']}: {self.error['message']}"
def encode_decimal(o):
if isinstance(o, decimal.Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
class AuthServiceProxy:
__id_count = 0
hostname: str
port: int
cookie: bytes
def __init__(
self,
hostname,
port,
cookie,
service_name=None,
timeout=HTTP_TIMEOUT,
connection=None,
ensure_ascii=True,
):
self.hostname = hostname
self.port = port
self.cookie = cookie
self.__timeout = timeout
self._service_name = service_name
self.ensure_ascii = ensure_ascii self.__auth_header = b"Basic " + base64.b64encode(cookie)
if connection:
self.__conn = connection
else:
self.__conn = httplib.HTTPConnection(self.hostname, port, timeout=timeout)
def reconnect(self):
self.__conn = httplib.HTTPConnection(
self.hostname, self.port, timeout=self.__timeout
)
def __getattr__(self, name):
if name.startswith("__") and name.endswith("__"):
raise AttributeError
if self._service_name is not None:
name = f"{self._service_name}.{name}"
return AuthServiceProxy(
self.hostname, self.port, self.cookie, name, connection=self.__conn
)
def _request(self, method, path, postdata):
headers = {
"Host": self.hostname,
"User-Agent": USER_AGENT,
"Authorization": self.__auth_header,
"Content-type": "application/json",
}
while True:
try:
self.__conn.request(method, path, postdata, headers)
return self._get_response()
except httplib.CannotSendRequest as e:
log.error(
"%s (port %s): Cannot send request: %s",
self.hostname,
self.port,
str(e),
)
self.reconnect()
except httplib.BadStatusLine as e:
if e.line == "''": self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
raise
except (BrokenPipeError, ConnectionResetError):
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
def __call__(self, *args):
AuthServiceProxy.__id_count += 1
log.debug(
"-%s-> %s %s",
AuthServiceProxy.__id_count,
self._service_name,
json.dumps(args, default=encode_decimal, ensure_ascii=self.ensure_ascii),
)
postdata = json.dumps(
{
"version": "1.1",
"method": self._service_name,
"params": args,
"id": AuthServiceProxy.__id_count,
},
default=encode_decimal,
ensure_ascii=self.ensure_ascii,
)
response = self._request("POST", "/", postdata.encode("utf-8"))
if response["error"] is not None:
raise JSONRPCException(response["error"])
if "result" not in response:
raise JSONRPCException({"code": -343, "message": "missing JSON-RPC result"})
return response["result"]
def _batch(self, rpc_call_list):
postdata = json.dumps(
list(rpc_call_list), default=encode_decimal, ensure_ascii=self.ensure_ascii
)
log.debug("--> %s", postdata)
return self._request("POST", "/", postdata.encode("utf-8"))
def _get_response(self):
http_response = self.__conn.getresponse()
if http_response is None:
raise JSONRPCException(
{"code": -342, "message": "missing HTTP response from server"}
)
content_type = http_response.getheader("Content-Type")
if content_type != "application/json":
raise JSONRPCException(
{
"code": -342,
"message": f"non-JSON HTTP response with '{http_response.status} {http_response.reason}' from server",
}
)
responsedata = http_response.read().decode("utf8")
response = json.loads(responsedata, parse_float=decimal.Decimal)
if "error" in response and response["error"] is None:
log.debug(
"<-%s- %s",
response["id"],
json.dumps(
response["result"],
default=encode_decimal,
ensure_ascii=self.ensure_ascii,
),
)
else:
log.debug("<-- %s", responsedata)
return response