tMerge pull request #3664 from SomberNight/json_rpc_pw - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit b076f45f8e78946e8bd0548b705c39db04d5fd2f DIR parent 1020449684a4a12eea85e7f51670a8eb35316873 HTML Author: ThomasV <thomasv@electrum.org> Date: Mon, 8 Jan 2018 00:28:27 +0100 Merge pull request #3664 from SomberNight/json_rpc_pw Password-protect the JSON RPC interface Diffstat: M lib/daemon.py | 38 +++++++++++++++++++++++++++---- A lib/jsonrpc.py | 95 ++++++++++++++++++++++++++++++ M lib/util.py | 8 ++++++++ 3 files changed, 137 insertions(+), 4 deletions(-) --- DIR diff --git a/lib/daemon.py b/lib/daemon.py t@@ -28,12 +28,12 @@ import time # from jsonrpc import JSONRPCResponseManager import jsonrpclib -from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer +from .jsonrpc import VerifyingJSONRPCServer from .version import ELECTRUM_VERSION from .network import Network from .util import json_decode, DaemonThread -from .util import print_error +from .util import print_error, to_string from .wallet import Wallet from .storage import WalletStorage from .commands import known_commands, Commands t@@ -75,7 +75,14 @@ def get_server(config): try: with open(lockfile) as f: (host, port), create_time = ast.literal_eval(f.read()) - server = jsonrpclib.Server('http://%s:%d' % (host, port)) + rpc_user, rpc_password = get_rpc_credentials(config) + if rpc_password == '': + # authentication disabled + server_url = 'http://%s:%d' % (host, port) + else: + server_url = 'http://%s:%s@%s:%d' % ( + rpc_user, rpc_password, host, port) + server = jsonrpclib.Server(server_url) # Test daemon is running server.ping() return server t@@ -87,6 +94,26 @@ def get_server(config): time.sleep(1.0) +def get_rpc_credentials(config): + rpc_user = config.get('rpcuser', None) + rpc_password = config.get('rpcpassword', None) + if rpc_user is None or rpc_password is None: + rpc_user = 'user' + import ecdsa, base64 + bits = 128 + nbytes = bits // 8 + (bits % 8 > 0) + pw_int = ecdsa.util.randrange(pow(2, bits)) + pw_b64 = base64.b64encode( + pw_int.to_bytes(nbytes, 'big'), b'-_') + rpc_password = to_string(pw_b64, 'ascii') + config.set_key('rpcuser', rpc_user) + config.set_key('rpcpassword', rpc_password, save=True) + elif rpc_password == '': + from .util import print_stderr + print_stderr('WARNING: RPC authentication is disabled.') + return rpc_user, rpc_password + + class Daemon(DaemonThread): def __init__(self, config, fd, is_gui): t@@ -109,8 +136,11 @@ class Daemon(DaemonThread): def init_server(self, config, fd, is_gui): host = config.get('rpchost', '127.0.0.1') port = config.get('rpcport', 0) + + rpc_user, rpc_password = get_rpc_credentials(config) try: - server = SimpleJSONRPCServer((host, port), logRequests=False) + server = VerifyingJSONRPCServer((host, port), logRequests=False, + rpc_user=rpc_user, rpc_password=rpc_password) except Exception as e: self.print_error('Warning: cannot initialize RPC server on host', host, e) self.server = None DIR diff --git a/lib/jsonrpc.py b/lib/jsonrpc.py t@@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 Thomas Voegtlin +# +# 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. + +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler +from base64 import b64decode +import time + +from . import util + + +class RPCAuthCredentialsInvalid(Exception): + def __str__(self): + return 'Authentication failed (bad credentials)' + + +class RPCAuthCredentialsMissing(Exception): + def __str__(self): + return 'Authentication failed (missing credentials)' + + +class RPCAuthUnsupportedType(Exception): + def __str__(self): + return 'Authentication failed (only basic auth is supported)' + + +# based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke +class VerifyingJSONRPCServer(SimpleJSONRPCServer): + + def __init__(self, *args, rpc_user, rpc_password, **kargs): + + self.rpc_user = rpc_user + self.rpc_password = rpc_password + + class VerifyingRequestHandler(SimpleJSONRPCRequestHandler): + def parse_request(myself): + # first, call the original implementation which returns + # True if all OK so far + if SimpleJSONRPCRequestHandler.parse_request(myself): + try: + self.authenticate(myself.headers) + return True + except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing, + RPCAuthUnsupportedType) as e: + myself.send_error(401, str(e)) + except BaseException as e: + import traceback, sys + traceback.print_exc(file=sys.stderr) + myself.send_error(500, str(e)) + return False + + SimpleJSONRPCServer.__init__( + self, requestHandler=VerifyingRequestHandler, *args, **kargs) + + def authenticate(self, headers): + if self.rpc_password == '': + # RPC authentication is disabled + return + + auth_string = headers.get('Authorization', None) + if auth_string is None: + raise RPCAuthCredentialsMissing() + + (basic, _, encoded) = auth_string.partition(' ') + if basic != 'Basic': + raise RPCAuthUnsupportedType() + + encoded = util.to_bytes(encoded, 'utf8') + credentials = util.to_string(b64decode(encoded), 'utf8') + (username, _, password) = credentials.partition(':') + if not (util.constant_time_compare(username, self.rpc_user) + and util.constant_time_compare(password, self.rpc_password)): + time.sleep(0.050) + raise RPCAuthCredentialsInvalid() DIR diff --git a/lib/util.py b/lib/util.py t@@ -28,6 +28,7 @@ from decimal import Decimal import traceback import urllib import threading +import hmac from .i18n import _ t@@ -202,6 +203,13 @@ def json_decode(x): except: return x + +# taken from Django Source Code +def constant_time_compare(val1, val2): + """Return True if the two strings are equal, False otherwise.""" + return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8')) + + # decorator that prints execution time def profiler(func): def do_profile(func, args, kw_args):