electrum-personal-server

Maximally lightweight electrum server for a single user
git clone https://git.parazyd.org/electrum-personal-server
Log | Files | Refs | README

commit 96b81af376581e3ce32317c8feb8d9e41357a185
parent 0d68b0827e7aac7c0202a1dc95d21a4353c6983e
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date:   Tue, 20 Mar 2018 11:40:49 +0000

Made the jsonrpc code use a persistent connection

Diffstat:
Mjsonrpc.py | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 97 insertions(+), 28 deletions(-)

diff --git a/jsonrpc.py b/jsonrpc.py @@ -1,59 +1,128 @@ -#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket/blob/master/joinmarket/jsonrpc.py -#copyright # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> and phelix / blockchained.com +# from https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/py3/jmclient/jmclient/jsonrpc.py + +# Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> +# Copyright (C) 2014 by phelix / blockchained.com +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. import base64 -import http.client +try: + import http.client as httplib +except ImportError: + import httplib import json + class JsonRpcError(Exception): + """ + The called method returned an error in the JSON-RPC response. + """ def __init__(self, obj): self.code = obj["code"] self.message = obj["message"] -class JsonRpcConnectionError(JsonRpcError): pass + +class JsonRpcConnectionError(Exception): + """ + Error thrown when the RPC connection itself failed. This means + that the server is either down or the connection settings + are wrong. + """ + pass + class JsonRpc(object): + """ + Simple implementation of a JSON-RPC client that is used + to connect to Bitcoin. + """ def __init__(self, host, port, user, password): self.host = host self.port = port - self.authstr = "%s:%s" % (user, password) + self.conn = httplib.HTTPConnection(self.host, self.port) + self.authstr = bytes("%s:%s" % (user, password), "utf-8") self.queryId = 1 def queryHTTP(self, obj): - headers = {"User-Agent": "electrum-personal-server", + """ + Send an appropriate HTTP query to the server. The JSON-RPC + request should be (as object) in 'obj'. If the call succeeds, + the resulting JSON object is returned. In case of an error + with the connection (not JSON-RPC itself), an exception is raised. + """ + headers = {"User-Agent": "joinmarket", "Content-Type": "application/json", "Accept": "application/json"} headers["Authorization"] = "Basic %s" % base64.b64encode( - self.authstr.encode()).decode() + self.authstr).decode("utf-8") body = json.dumps(obj) - try: - conn = http.client.HTTPConnection(self.host, self.port) - conn.request("POST", "", body, headers) - response = conn.getresponse() - if response.status == 401: - conn.close() - raise JsonRpcConnectionError( - "authentication for JSON-RPC failed") - # All of the codes below are 'fine' from a JSON-RPC point of view. - if response.status not in [200, 404, 500]: - conn.close() - raise JsonRpcConnectionError("unknown error in JSON-RPC") - data = response.read() - conn.close() - return json.loads(data.decode()) - except JsonRpcConnectionError as exc: - raise exc - except Exception as exc: - raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" + - repr(exc)) + while True: + try: + self.conn.request("POST", "", body, headers) + response = self.conn.getresponse() + if response.status == 401: + self.conn.close() + raise JsonRpcConnectionError( + "authentication for JSON-RPC failed") + # All of below codes are 'fine' from a JSON-RPC point of view. + if response.status not in [200, 404, 500]: + self.conn.close() + raise JsonRpcConnectionError("unknown error in JSON-RPC") + data = response.read() + return json.loads(data.decode("utf-8")) + except JsonRpcConnectionError as exc: + raise exc + except httplib.BadStatusLine: + return "CONNFAILURE" + except Exception as exc: + if str(exc) == "Connection reset by peer": + self.conn.connect() + continue + else: + raise JsonRpcConnectionError("JSON-RPC connection failed" + + ". Err:" + repr(exc)) + break def call(self, method, params): + """ + Call a method over JSON-RPC. + """ currentId = self.queryId self.queryId += 1 request = {"method": method, "params": params, "id": currentId} - response = self.queryHTTP(request) + #query can fail from keepalive timeout; keep retrying if it does, up + #to a reasonable limit, then raise (failure to access blockchain + #is a critical failure). Note that a real failure to connect (e.g. + #wrong port) is raised in queryHTTP directly. + response_received = False + for i in range(100): + response = self.queryHTTP(request) + if response != "CONNFAILURE": + response_received = True + break + #Failure means keepalive timed out, just make a new one + self.conn = httplib.HTTPConnection(self.host, self.port) + if not response_received: + raise JsonRpcConnectionError("Unable to connect over RPC") if response["id"] != currentId: raise JsonRpcConnectionError("invalid id returned by query") if response["error"] is not None: raise JsonRpcError(response["error"]) return response["result"] +