commit dce6bff4768756c4ac2b0856d29ff9fe08348a15
parent 941fce582114f5b8d93780779b526dc76c5c8c0a
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date: Wed, 13 May 2020 18:13:50 +0100
Handle losing connection to bitcoin node
Previously if the json-rpc connection to the bitcoin node was lost then
the server would crash. Now it will close the Electrum connection and
refuse all new connections until it reestablishes a link to the node.
Electrum will then display a red dot as an indication that something is
wrong, and so the server operator can be reminded to restart the node.
Also, the json-rpc functions will no longer cache the username and
password values obtained from the cookie file. Then if the node is
restarted and generates a new cookie then the server will correctly
use the new authentication information.
Diffstat:
2 files changed, 94 insertions(+), 49 deletions(-)
diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py
@@ -8,6 +8,7 @@ import logging
import tempfile
import platform
import json
+import traceback
from json.decoder import JSONDecodeError
from configparser import RawConfigParser, NoSectionError, NoOptionError
from ipaddress import ip_network, ip_address
@@ -30,14 +31,20 @@ bestblockhash = [None]
def on_heartbeat_listening(txmonitor):
logger = logging.getLogger('ELECTRUMPERSONALSERVER')
- txmonitor.check_for_updated_txes()
+ try:
+ txmonitor.check_for_updated_txes()
+ is_node_reachable = True
+ except JsonRpcError:
+ is_node_reachable = False
+ return is_node_reachable
def on_heartbeat_connected(rpc, txmonitor, protocol):
logger = logging.getLogger('ELECTRUMPERSONALSERVER')
is_tip_updated, header = check_for_new_blockchain_tip(rpc,
protocol.are_headers_raw)
if is_tip_updated:
- logger.debug("Blockchain tip updated")
+ logger.debug("Blockchain tip updated " + (str(header["height"]) if
+ "height" in header else ""))
protocol.on_blockchain_tip_updated(header)
updated_scripthashes = txmonitor.check_for_updated_txes()
protocol.on_updated_scripthashes(updated_scripthashes)
@@ -90,35 +97,44 @@ def run_electrum_server(rpc, txmonitor, config):
server_sock = create_server_socket(hostport)
server_sock.settimeout(poll_interval_listening)
+ accepting_clients = True
while True:
- try:
- sock = None
- while sock == None:
- try:
- sock, addr = server_sock.accept()
- if not any([ip_address(addr[0]) in ipnet
- for ipnet in ip_whitelist]):
- logger.debug(addr[0] + " not in whitelist, closing")
- raise ConnectionRefusedError()
- sock = ssl.wrap_socket(sock, server_side=True,
- certfile=certfile, keyfile=keyfile,
- ssl_version=ssl.PROTOCOL_SSLv23)
- except socket.timeout:
- on_heartbeat_listening(txmonitor)
- except (ConnectionRefusedError, ssl.SSLError):
- sock.close()
- sock = None
- logger.debug('Electrum connected from ' + str(addr[0]))
-
- def send_reply_fun(reply):
- line = json.dumps(reply)
- sock.sendall(line.encode('utf-8') + b'\n')
- logger.debug('<= ' + line)
- protocol.set_send_reply_fun(send_reply_fun)
+ # main server loop, runs forever
+ sock = None
+ while sock == None:
+ # loop waiting for a successful connection from client
+ try:
+ sock, addr = server_sock.accept()
+ if not accepting_clients:
+ logger.debug("Refusing connection from client because"
+ + " Bitcoin node isnt reachable")
+ raise ConnectionRefusedError()
+ if not any([ip_address(addr[0]) in ipnet
+ for ipnet in ip_whitelist]):
+ logger.debug(addr[0] + " not in whitelist, closing")
+ raise ConnectionRefusedError()
+ sock = ssl.wrap_socket(sock, server_side=True,
+ certfile=certfile, keyfile=keyfile,
+ ssl_version=ssl.PROTOCOL_SSLv23)
+ except socket.timeout:
+ is_node_reachable = on_heartbeat_listening(txmonitor)
+ accepting_clients = is_node_reachable
+ except (ConnectionRefusedError, ssl.SSLError):
+ sock.close()
+ sock = None
+ logger.debug('Electrum connected from ' + str(addr[0]))
+
+ def send_reply_fun(reply):
+ line = json.dumps(reply)
+ sock.sendall(line.encode('utf-8') + b'\n')
+ logger.debug('<= ' + line)
+ protocol.set_send_reply_fun(send_reply_fun)
+ try:
sock.settimeout(poll_interval_connected)
recv_buffer = bytearray()
while True:
+ # loop for replying to client queries
try:
recv_data = sock.recv(4096)
if not recv_data or len(recv_data) == 0:
@@ -140,6 +156,10 @@ def run_electrum_server(rpc, txmonitor, config):
protocol.handle_query(query)
except socket.timeout:
on_heartbeat_connected(rpc, txmonitor, protocol)
+ except JsonRpcError as e:
+ logger.debug("Error with node connection, e = " + repr(e)
+ + "\ntraceback = " + str(traceback.format_exc()))
+ accepting_clients = False
except (IOError, EOFError) as e:
if isinstance(e, (EOFError, ConnectionRefusedError)):
logger.debug("Electrum wallet disconnected")
@@ -150,9 +170,8 @@ def run_electrum_server(rpc, txmonitor, config):
sock.close()
except IOError:
pass
- sock = None
- protocol.on_disconnect()
- time.sleep(0.2)
+ protocol.on_disconnect()
+ time.sleep(0.2)
def get_scriptpubkeys_to_monitor(rpc, config):
logger = logging.getLogger('ELECTRUMPERSONALSERVER')
@@ -274,7 +293,7 @@ def get_certs(config):
raise ValueError('invalid cert: {}, key: {}'.format(
certfile, keyfile))
-def obtain_rpc_username_password(datadir):
+def obtain_cookie_file_path(datadir):
logger = logging.getLogger('ELECTRUMPERSONALSERVER')
if len(datadir.strip()) == 0:
logger.debug("no datadir configuration, checking in default location")
@@ -291,11 +310,8 @@ def obtain_rpc_username_password(datadir):
if not os.path.exists(cookie_path):
logger.warning("Unable to find .cookie file, try setting `datadir`" +
" config")
- return None, None
- fd = open(cookie_path)
- username, password = fd.read().strip().split(":")
- fd.close()
- return username, password
+ return None
+ return cookie_path
def parse_args():
from argparse import ArgumentParser
@@ -349,19 +365,22 @@ def main():
SERVER_VERSION_NUMBER))
logger.info('Logging to ' + logfilename)
logger.debug("Process ID (PID) = " + str(os.getpid()))
+ rpc_u = None
+ rpc_p = None
+ cookie_path = None
try:
rpc_u = config.get("bitcoin-rpc", "rpc_user")
rpc_p = config.get("bitcoin-rpc", "rpc_password")
logger.debug("obtaining auth from rpc_user/pass")
except NoOptionError:
- rpc_u, rpc_p = obtain_rpc_username_password(config.get(
+ cookie_path = obtain_cookie_file_path(config.get(
"bitcoin-rpc", "datadir"))
logger.debug("obtaining auth from .cookie")
- if rpc_u == None:
+ if rpc_u == None and cookie_path == None:
return
rpc = JsonRpc(host = config.get("bitcoin-rpc", "host"),
port = int(config.get("bitcoin-rpc", "port")),
- user = rpc_u, password = rpc_p,
+ user = rpc_u, password = rpc_p, cookie_path = cookie_path,
wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip(),
logger=logger)
diff --git a/electrumpersonalserver/server/jsonrpc.py b/electrumpersonalserver/server/jsonrpc.py
@@ -15,12 +15,18 @@ class JsonRpc(object):
Simple implementation of a JSON-RPC client that is used
to connect to Bitcoin.
"""
- def __init__(self, host, port, user, password, wallet_filename="",
- logger=None):
+ def __init__(self, host, port, user, password, cookie_path=None,
+ wallet_filename="", logger=None):
self.host = host
self.port = port
+
+ self.cookie_path = cookie_path
+ if cookie_path:
+ self.load_from_cookie()
+ else:
+ self.create_authstr(user, password)
+
self.conn = http.client.HTTPConnection(self.host, self.port)
- self.authstr = "%s:%s" % (user, password)
if len(wallet_filename) > 0:
self.url = "/wallet/" + wallet_filename
else:
@@ -28,6 +34,15 @@ class JsonRpc(object):
self.logger = logger
self.queryId = 1
+ def create_authstr(self, username, password):
+ self.authstr = "%s:%s" % (username, password)
+
+ def load_from_cookie(self):
+ fd = open(self.cookie_path)
+ username, password = fd.read().strip().split(":")
+ fd.close()
+ self.create_authstr(username, password)
+
def queryHTTP(self, obj):
"""
Send an appropriate HTTP query to the server. The JSON-RPC
@@ -41,14 +56,22 @@ class JsonRpc(object):
headers["Authorization"] = (b"Basic " +
base64.b64encode(self.authstr.encode('utf-8')))
body = json.dumps(obj)
+ auth_failed_once = False
for i in range(20):
try:
self.conn.request("POST", self.url, body, headers)
response = self.conn.getresponse()
if response.status == 401:
- self.conn.close()
- raise JsonRpcConnectionError(
- "authentication for JSON-RPC failed")
+ if self.cookie_path == None or auth_failed_once:
+ self.conn.close()
+ raise JsonRpcConnectionError(
+ "authentication for JSON-RPC failed")
+ else:
+ auth_failed_once = True
+ #try reloading u/p from the cookie file once
+ self.load_from_cookie()
+ raise OSError() #jump to error handler below
+ auth_failed_once = False
#All the codes below are 'fine' from a JSON-RPC point of view.
if response.status not in [200, 404, 500]:
self.conn.close()
@@ -59,11 +82,14 @@ class JsonRpc(object):
raise exc
except http.client.BadStatusLine:
return "CONNFAILURE"
- except OSError as e:
- self.logger.debug('Reconnecting RPC after dropped ' +
- 'connection: ' + repr(e))
- self.conn.close()
- self.conn.connect()
+ except OSError:
+ # connection dropped, reconnect
+ try:
+ self.conn.close()
+ self.conn.connect()
+ except ConnectionError as e:
+ #node probably offline, notify with jsonrpc error
+ raise JsonRpcConnectionError(repr(e))
continue
except Exception as exc:
raise JsonRpcConnectionError("JSON-RPC connection failed. Err:"