diff --git a/basicswap/rpc.py b/basicswap/rpc.py index 3fbbc53..dd2bc78 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -12,8 +12,9 @@ import logging import traceback import subprocess from xmlrpc.client import ( - Transport, Fault, + Transport, + SafeTransport, ) from .util import jsonDecimal @@ -39,15 +40,15 @@ class Jsonrpc(): # get the url parsed = urllib.parse.urlparse(uri) - if parsed.scheme not in ("http", "https"): - raise OSError("unsupported XML-RPC protocol") + if parsed.scheme not in ('http', 'https'): + raise OSError('unsupported XML-RPC protocol') self.__host = parsed.netloc self.__handler = parsed.path if not self.__handler: - self.__handler = "/RPC2" + self.__handler = '/RPC2' if transport is None: - handler = Transport + handler = SafeTransport if parsed.scheme == 'https' else Transport extra_kwargs = {} transport = handler(use_datetime=use_datetime, use_builtin_types=use_builtin_types, @@ -58,6 +59,8 @@ class Jsonrpc(): self.__verbose = verbose self.__allow_none = allow_none + self.__request_id = 1 + def close(self): if self.__transport is not None: self.__transport.close() @@ -70,14 +73,15 @@ class Jsonrpc(): request_body = { 'method': method, 'params': params, - 'id': 2 + 'id': self.__request_id } - connection.putrequest("POST", self.__handler) - headers.append(("Content-Type", "application/json")) - headers.append(("User-Agent", 'jsonrpc')) + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('User-Agent', 'jsonrpc')) self.__transport.send_headers(connection, headers) self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8')) + self.__request_id += 1 resp = connection.getresponse() return resp.read() diff --git a/basicswap/rpc_xmr.py b/basicswap/rpc_xmr.py index 0937f93..4fe2c99 100644 --- a/basicswap/rpc_xmr.py +++ b/basicswap/rpc_xmr.py @@ -1,7 +1,162 @@ # -*- coding: utf-8 -*- +import os import json -import requests +import time +import urllib +import hashlib +from xmlrpc.client import ( + Fault, + Transport, + SafeTransport, +) +from .util import jsonDecimal + + +class JsonrpcDigest(): + # __getattr__ complicates extending ServerProxy + def __init__(self, uri, transport=None, encoding=None, verbose=False, + allow_none=False, use_datetime=False, use_builtin_types=False, + *, context=None): + + parsed = urllib.parse.urlparse(uri) + if parsed.scheme not in ('http', 'https'): + raise OSError('unsupported XML-RPC protocol') + self.__host = parsed.netloc + self.__handler = parsed.path + + if transport is None: + handler = SafeTransport if parsed.scheme == 'https' else Transport + extra_kwargs = {} + transport = handler(use_datetime=use_datetime, + use_builtin_types=use_builtin_types, + **extra_kwargs) + self.__transport = transport + + self.__encoding = encoding or 'utf-8' + self.__verbose = verbose + self.__allow_none = allow_none + + self.__request_id = 1 + + def close(self): + if self.__transport is not None: + self.__transport.close() + + def post_request(self, method, params, timeout=None): + try: + connection = self.__transport.make_connection(self.__host) + if timeout: + connection.timeout = timeout + headers = self.__transport._extra_headers[:] + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('User-Agent', 'jsonrpc')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8')) + self.__request_id += 1 + + resp = connection.getresponse() + return resp.read() + + except Fault: + raise + except Exception: + self.__transport.close() + raise + + def json_request(self, method, params, username='', password='', timeout=None): + try: + connection = self.__transport.make_connection(self.__host) + if timeout: + connection.timeout = timeout + + headers = self.__transport._extra_headers[:] + + request_body = { + 'method': method, + 'params': params, + 'jsonrpc': '2.0', + 'id': self.__request_id + } + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('Connection', 'keep-alive')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8')) + resp = connection.getresponse() + + if resp.status == 401: + resp_headers = resp.getheaders() + v = resp.read() + + algorithm = '' + realm = '' + nonce = '' + for h in resp_headers: + if h[0] != 'WWW-authenticate': + continue + fields = h[1].split(',') + for f in fields: + key, value = f.split('=', 1) + if key == 'algorithm' and value != 'MD5': + break + if key == 'realm': + realm = value.strip('"') + if key == 'nonce': + nonce = value.strip('"') + if realm != '' and nonce != '': + break + + if realm == '' or nonce == '': + raise ValueError('Authenticate header not found.') + + path = self.__handler + HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest() + + http_method = 'POST' + HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest() + + ncvalue = '{:08x}'.format(1) + s = ncvalue.encode('utf-8') + s += nonce.encode('utf-8') + s += time.ctime().encode('utf-8') + s += os.urandom(8) + cnonce = (hashlib.sha1(s).hexdigest()[:16]) + + # MD5-SESS + HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest() + + respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest() + + header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"' + headers = self.__transport._extra_headers[:] + headers.append(('Authorization', header_value)) + + request_body = { + 'method': method, + 'params': params, + 'jsonrpc': '2.0', + 'id': self.__request_id + } + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('Connection', 'keep-alive')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8')) + resp = connection.getresponse() + + self.__request_id += 1 + return resp.read() + + except Fault: + raise + except Exception: + self.__transport.close() + raise def callrpc_xmr(rpc_port, auth, method, params=[], rpc_host='127.0.0.1', path='json_rpc', timeout=120): @@ -11,17 +166,11 @@ def callrpc_xmr(rpc_port, auth, method, params=[], rpc_host='127.0.0.1', path='j url = '{}:{}/{}'.format(rpc_host, rpc_port, path) else: url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path) - request_body = { - 'method': method, - 'params': params, - 'id': 2, - 'jsonrpc': '2.0' - } - headers = { - 'Content-Type': 'application/json' - } - p = requests.post(url, data=json.dumps(request_body), auth=requests.auth.HTTPDigestAuth(auth[0], auth[1]), headers=headers, timeout=timeout) - r = json.loads(p.text) + + x = JsonrpcDigest(url) + v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout) + x.close() + r = json.loads(v.decode('utf-8')) except Exception as ex: raise ValueError('RPC Server Error: {}'.format(str(ex))) @@ -37,17 +186,11 @@ def callrpc_xmr_na(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json url = '{}:{}/{}'.format(rpc_host, rpc_port, path) else: url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path) - request_body = { - 'method': method, - 'params': params, - 'id': 2, - 'jsonrpc': '2.0' - } - headers = { - 'Content-Type': 'application/json' - } - p = requests.post(url, data=json.dumps(request_body), headers=headers, timeout=timeout) - r = json.loads(p.text) + + x = JsonrpcDigest(url) + v = x.json_request(method, params, timeout=timeout) + x.close() + r = json.loads(v.decode('utf-8')) except Exception as ex: raise ValueError('RPC Server Error: {}'.format(str(ex))) @@ -63,14 +206,11 @@ def callrpc_xmr2(rpc_port, method, params=None, rpc_host='127.0.0.1', timeout=12 url = '{}:{}/{}'.format(rpc_host, rpc_port, method) else: url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method) - headers = { - 'Content-Type': 'application/json' - } - if params is None: - p = requests.post(url, headers=headers, timeout=timeout) - else: - p = requests.post(url, data=json.dumps(params), headers=headers, timeout=timeout) - r = json.loads(p.text) + + x = JsonrpcDigest(url) + v = x.post_request(method, params, timeout=timeout) + x.close() + r = json.loads(v.decode('utf-8')) except Exception as ex: raise ValueError('RPC Server Error: {}'.format(str(ex)))