diff --git a/basicswap/base.py b/basicswap/base.py index 2f2cd60..891893a 100644 --- a/basicswap/base.py +++ b/basicswap/base.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 tecnovert +# Copyright (c) 2019-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import os import shlex +import socks +import socket +import urllib import logging import threading import subprocess @@ -25,6 +28,10 @@ from .chainparams import ( ) +def getaddrinfo_tor(*args): + return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))] + + class BaseApp: def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'): self.log_name = log_name @@ -44,6 +51,15 @@ class BaseApp: self.prepareLogging() self.log.info('Network: {}'.format(self.chain)) + self.use_tor_proxy = self.settings.get('use_tor', False) + self.tor_proxy_host = self.settings.get('tor_proxy_host', '127.0.0.1') + self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050) + self.tor_control_password = self.settings.get('tor_control_password', None) + self.tor_control_port = self.settings.get('tor_control_port', 9051) + self.default_socket = socket.socket + self.default_socket_timeout = socket.getdefaulttimeout() + self.default_socket_getaddrinfo = socket.getaddrinfo + def stopRunning(self, with_code=0): self.fail_code = with_code with self.mxDB: @@ -139,3 +155,38 @@ class BaseApp: return True str_error = str(ex).lower() return 'read timed out' in str_error or 'no connection to daemon' in str_error + + def setConnectionParameters(self, timeout=120): + opener = urllib.request.build_opener() + opener.addheaders = [('User-agent', 'Mozilla/5.0')] + urllib.request.install_opener(opener) + + if self.use_tor_proxy: + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port, rdns=True) + socket.socket = socks.socksocket + socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail + + socket.setdefaulttimeout(timeout) + + def popConnectionParameters(self): + if self.use_tor_proxy: + socket.socket = self.default_socket + socket.getaddrinfo = self.default_socket_getaddrinfo + socket.setdefaulttimeout(self.default_socket_timeout) + + def torControl(self, query): + try: + command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(self.tor_control_password, query).encode('utf-8') + c = socket.create_connection((self.tor_proxy_host, self.tor_control_port)) + c.send(command) + response = bytearray() + while True: + rv = c.recv(1024) + if not rv: + break + response += rv + c.close() + return response + except Exception as e: + self.log.error(f'torControl {e}') + return diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 7856288..892831d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -5659,74 +5659,78 @@ class BasicSwap(BaseApp): def lookupRates(self, coin_from, coin_to): self.log.debug('lookupRates {}, {}'.format(coin_from, coin_to)) - rv = {} - ci_from = self.ci(int(coin_from)) - ci_to = self.ci(int(coin_to)) + try: + self.setConnectionParameters() + rv = {} + ci_from = self.ci(int(coin_from)) + ci_to = self.ci(int(coin_to)) - headers = {'Connection': 'close'} - name_from = ci_from.chainparams()['name'] - name_to = ci_to.chainparams()['name'] - url = 'https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd'.format(name_from, name_to) - start = time.time() - req = urllib.request.Request(url, headers=headers) - js = json.loads(urllib.request.urlopen(req, timeout=10).read()) - js['time_taken'] = time.time() - start - rate = float(js[name_from]['usd']) / float(js[name_to]['usd']) - js['rate_inferred'] = ci_to.format_amount(rate, conv_int=True, r=1) - rv['coingecko'] = js - - ticker_from = ci_from.chainparams()['ticker'] - ticker_to = ci_to.chainparams()['ticker'] - if ci_from.coin_type() == Coins.BTC: - pair = '{}-{}'.format(ticker_from, ticker_to) - url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair + headers = {'Connection': 'close'} + name_from = ci_from.chainparams()['name'] + name_to = ci_to.chainparams()['name'] + url = 'https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd'.format(name_from, name_to) start = time.time() req = urllib.request.Request(url, headers=headers) js = json.loads(urllib.request.urlopen(req, timeout=10).read()) js['time_taken'] = time.time() - start - js['pair'] = pair + rate = float(js[name_from]['usd']) / float(js[name_to]['usd']) + js['rate_inferred'] = ci_to.format_amount(rate, conv_int=True, r=1) + rv['coingecko'] = js - try: - rate_inverted = ci_from.make_int(1.0 / float(js['result']['Last']), r=1) - js['rate_inferred'] = ci_to.format_amount(rate_inverted) - except Exception as e: - self.log.warning('lookupRates error: %s', str(e)) - js['rate_inferred'] = 'error' + ticker_from = ci_from.chainparams()['ticker'] + ticker_to = ci_to.chainparams()['ticker'] + if ci_from.coin_type() == Coins.BTC: + pair = '{}-{}'.format(ticker_from, ticker_to) + url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair + start = time.time() + req = urllib.request.Request(url, headers=headers) + js = json.loads(urllib.request.urlopen(req, timeout=10).read()) + js['time_taken'] = time.time() - start + js['pair'] = pair - rv['bittrex'] = js - elif ci_to.coin_type() == Coins.BTC: - pair = '{}-{}'.format(ticker_to, ticker_from) - url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair - start = time.time() - req = urllib.request.Request(url, headers=headers) - js = json.loads(urllib.request.urlopen(req, timeout=10).read()) - js['time_taken'] = time.time() - start - js['pair'] = pair - js['rate_last'] = js['result']['Last'] - rv['bittrex'] = js - else: - pair = 'BTC-{}'.format(ticker_from) - url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair - start = time.time() - req = urllib.request.Request(url, headers=headers) - js_from = json.loads(urllib.request.urlopen(req, timeout=10).read()) - js_from['time_taken'] = time.time() - start - js_from['pair'] = pair + try: + rate_inverted = ci_from.make_int(1.0 / float(js['result']['Last']), r=1) + js['rate_inferred'] = ci_to.format_amount(rate_inverted) + except Exception as e: + self.log.warning('lookupRates error: %s', str(e)) + js['rate_inferred'] = 'error' - pair = 'BTC-{}'.format(ticker_to) - url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair - start = time.time() - req = urllib.request.Request(url, headers=headers) - js_to = json.loads(urllib.request.urlopen(req, timeout=10).read()) - js_to['time_taken'] = time.time() - start - js_to['pair'] = pair + rv['bittrex'] = js + elif ci_to.coin_type() == Coins.BTC: + pair = '{}-{}'.format(ticker_to, ticker_from) + url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair + start = time.time() + req = urllib.request.Request(url, headers=headers) + js = json.loads(urllib.request.urlopen(req, timeout=10).read()) + js['time_taken'] = time.time() - start + js['pair'] = pair + js['rate_last'] = js['result']['Last'] + rv['bittrex'] = js + else: + pair = 'BTC-{}'.format(ticker_from) + url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair + start = time.time() + req = urllib.request.Request(url, headers=headers) + js_from = json.loads(urllib.request.urlopen(req, timeout=10).read()) + js_from['time_taken'] = time.time() - start + js_from['pair'] = pair - try: - rate_inferred = float(js_from['result']['Last']) / float(js_to['result']['Last']) - rate_inferred = ci_to.format_amount(rate, conv_int=True, r=1) - except Exception as e: - rate_inferred = 'error' + pair = 'BTC-{}'.format(ticker_to) + url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair + start = time.time() + req = urllib.request.Request(url, headers=headers) + js_to = json.loads(urllib.request.urlopen(req, timeout=10).read()) + js_to['time_taken'] = time.time() - start + js_to['pair'] = pair - rv['bittrex'] = {'from': js_from, 'to': js_to, 'rate_inferred': rate_inferred} + try: + rate_inferred = float(js_from['result']['Last']) / float(js_to['result']['Last']) + rate_inferred = ci_to.format_amount(rate, conv_int=True, r=1) + except Exception as e: + rate_inferred = 'error' - return rv + rv['bittrex'] = {'from': js_from, 'to': js_to, 'rate_inferred': rate_inferred} + + return rv + finally: + self.popConnectionParameters() diff --git a/basicswap/explorers.py b/basicswap/explorers.py index 0f090a4..c0fff03 100644 --- a/basicswap/explorers.py +++ b/basicswap/explorers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 tecnovert +# Copyright (c) 2019-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -17,9 +17,12 @@ class Explorer(): def readURL(self, url): self.log.debug('Explorer url: {}'.format(url)) - headers = {'User-Agent': 'Mozilla/5.0'} - req = urllib.request.Request(url, headers=headers) - return urllib.request.urlopen(req).read() + try: + self.swapclient.setConnectionParameters() + req = urllib.request.Request(url) + return urllib.request.urlopen(req).read() + finally: + self.swapclient.popConnectionParameters() class ExplorerInsight(Explorer): diff --git a/basicswap/http_server.py b/basicswap/http_server.py index 8c3e270..07e5be9 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -49,7 +49,7 @@ from .js_server import ( js_rate, js_index, ) -from .ui import ( +from .ui.util import ( PAGE_LIMIT, inputAmount, describeBid, @@ -59,6 +59,7 @@ from .ui import ( have_data_entry, get_data_entry_or, ) +from .ui.page_tor import page_tor env = Environment(loader=PackageLoader('basicswap', 'templates')) @@ -1421,6 +1422,7 @@ class HttpHandler(BaseHTTPRequestHandler): h2=self.server.title, version=__version__, summary=summary, + use_tor_proxy=swap_client.use_tor_proxy, shutdown_token=shutdown_token ), 'UTF-8') @@ -1519,6 +1521,8 @@ class HttpHandler(BaseHTTPRequestHandler): return self.page_smsgaddresses(url_split, post_string) if url_split[1] == 'identity': return self.page_identity(url_split, post_string) + if url_split[1] == 'tor': + return page_tor(self, url_split, post_string) if url_split[1] == 'shutdown': return self.page_shutdown(url_split, post_string) return self.page_index(url_split) @@ -1562,6 +1566,7 @@ class HttpThread(threading.Thread, HTTPServer): self.title = 'BasicSwap, ' + self.swap_client.chain self.last_form_id = dict() self.session_tokens = dict() + self.env = env self.timeout = 60 HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 385f46c..8d7e248 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -17,7 +17,7 @@ from .basicswap_util import ( from .chainparams import ( Coins, ) -from .ui import ( +from .ui.util import ( PAGE_LIMIT, getCoinType, inputAmount, diff --git a/basicswap/templates/index.html b/basicswap/templates/index.html index 41c7b57..722ccab 100644 --- a/basicswap/templates/index.html +++ b/basicswap/templates/index.html @@ -13,12 +13,13 @@ Version: {{ version }} Explorers
SMSG Addresses

-Swaps in progress: {{ summary.num_swapping }}
+Swaps in Progress: {{ summary.num_swapping }}
Network Offers: {{ summary.num_network_offers }}
Sent Offers: {{ summary.num_sent_offers }}
Received Bids: {{ summary.num_recv_bids }}
Sent Bids: {{ summary.num_sent_bids }}
Watched Outputs: {{ summary.num_watched_outputs }}
+{% if use_tor_proxy %} TOR Information
{% endif %}

New Offer

diff --git a/basicswap/templates/tor.html b/basicswap/templates/tor.html new file mode 100644 index 0000000..e83e945 --- /dev/null +++ b/basicswap/templates/tor.html @@ -0,0 +1,15 @@ +{% include 'header.html' %} + +

TOR Information

+{% if refresh %} +

Page Refresh: {{ refresh }} seconds

+{% endif %} + + + + + +
Circuit Established{{ data.circuit_established }}
Bytes Written{{ data.bytes_written }}
Bytes Read{{ data.bytes_read }}
+ +

home

+ diff --git a/basicswap/ui/__init__.py b/basicswap/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/basicswap/ui/page_tor.py b/basicswap/ui/page_tor.py new file mode 100644 index 0000000..50dfb2b --- /dev/null +++ b/basicswap/ui/page_tor.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import os + + +def extract_data(bytes_in): + str_in = bytes_in.decode('utf-8') + start = str_in.find('=') + if start < 0: + return None + start += 1 + end = str_in.find('\r', start) + if end < 0: + return None + return str_in[start: end] + + +def page_tor(self, url_split, post_string): + template = self.server.env.get_template('tor.html') + + swap_client = self.server.swap_client + + page_data = {} + + rv = swap_client.torControl('GETINFO status/circuit-established') + page_data['circuit_established'] = extract_data(rv) + + rv = swap_client.torControl('GETINFO traffic/read') + page_data['bytes_written'] = extract_data(rv) + + rv = swap_client.torControl('GETINFO traffic/written') + page_data['bytes_read'] = extract_data(rv) + + messages = [] + + return bytes(template.render( + title=self.server.title, + h2=self.server.title, + messages=messages, + data=page_data, + form_id=os.urandom(8).hex(), + ), 'UTF-8') diff --git a/basicswap/ui.py b/basicswap/ui/util.py similarity index 98% rename from basicswap/ui.py rename to basicswap/ui/util.py index 7e68434..34053c6 100644 --- a/basicswap/ui.py +++ b/basicswap/ui/util.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021 tecnovert +# Copyright (c) 2020-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import json import traceback -from .util import ( +from basicswap.util import ( make_int, format_timestamp, ) -from .chainparams import ( +from basicswap.chainparams import ( Coins, ) -from .basicswap_util import ( +from basicswap.basicswap_util import ( TxTypes, TxStates, BidStates, @@ -26,7 +26,7 @@ from .basicswap_util import ( getLastBidState, ) -from .protocols.xmr_swap_1 import getChainBSplitKey +from basicswap.protocols.xmr_swap_1 import getChainBSplitKey PAGE_LIMIT = 50 diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 32f9297..ea45c3d 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -28,6 +28,7 @@ from basicswap.rpc import ( callrpc_cli, waitForRPC, ) +from basicswap.base import getaddrinfo_tor from basicswap.basicswap import BasicSwap from basicswap.chainparams import Coins from basicswap.util import toBool @@ -84,7 +85,7 @@ BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19796)) NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19798)) PART_ONION_PORT = int(os.getenv('PART_ONION_PORT', 51734)) -LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 19795)) # Still on 0.18 codebase, same port +LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) # Still on 0.18 codebase, same port BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334)) PART_RPC_USER = os.getenv('PART_RPC_USER', '') @@ -129,10 +130,6 @@ def make_reporthook(): return reporthook -def getaddrinfo(*args): - return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))] - - def setConnectionParameters(): opener = urllib.request.build_opener() opener.addheaders = [('User-agent', 'Mozilla/5.0')] @@ -141,7 +138,7 @@ def setConnectionParameters(): if use_tor_proxy: socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, TOR_PROXY_HOST, TOR_PROXY_PORT, rdns=True) socket.socket = socks.socksocket - socket.getaddrinfo = getaddrinfo # Without this accessing .onion links would fail + socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail # Set low timeout for urlretrieve connections socket.setdefaulttimeout(5) @@ -386,6 +383,21 @@ def prepareCore(coin, version_pair, settings, data_dir): extractCore(coin, version_pair, settings, bin_dir, release_path) +def writeTorSettings(fp, coin, coin_settings, tor_control_password): + onionport = coin_settings['onionport'] + fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n') + if coin == 'particl': + # TODO: lookuptorcontrolhost is default behaviour in later BTC versions + fp.write(f'torpassword={tor_control_password}\n') + fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n') + fp.write('lookuptorcontrolhost=any\n') # Particl only option + + if coin == 'litecoin': + fp.write(f'bind=0.0.0.0:{onionport}\n') + else: + fp.write(f'bind=0.0.0.0:{onionport}=onion\n') + + def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False, tor_control_password=None): core_settings = settings['chainclients'][coin] bin_dir = core_settings['bindir'] @@ -467,12 +479,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False fp.write('wallet=wallet.dat\n') if tor_control_password is not None: - onionport = core_settings['onionport'] - fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n') - fp.write(f'torpassword={tor_control_password}\n') - fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n') - # -listen is automatically set in InitParameterInteraction when bind is set - fp.write(f'bind=0.0.0.0:{onionport}=onion\n') + writeTorSettings(fp, coin, core_settings, tor_control_password) salt = generate_salt(16) if coin == 'particl': @@ -523,10 +530,10 @@ def write_torrc(data_dir, tor_control_password): def addTorSettings(settings, tor_control_password): - settings['tor_control_password'] = tor_control_password settings['use_tor'] = True settings['tor_proxy_host'] = TOR_PROXY_HOST settings['tor_proxy_port'] = TOR_PROXY_PORT + settings['tor_control_password'] = tor_control_password settings['tor_control_port'] = TOR_CONTROL_PORT @@ -547,7 +554,7 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False): shutil.copyfile(core_conf_path, core_conf_path + '.last') shutil.copyfile(wallet_conf_path, wallet_conf_path + '.last') - daemon_tor_settings = ('proxy=', 'proxy-allow-dns-leak=', 'no-igd=') + daemon_tor_settings = ('proxy=', 'proxy-allow-dns-leaks=', 'no-igd=') with open(core_conf_path, 'w') as fp: with open(core_conf_path + '.last') as fp_in: # Disable tor first @@ -613,11 +620,7 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False): if not skip_line: fp.write(line) if enable: - onionport = coin_settings['onionport'] - fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n') - fp.write(f'torpassword={tor_control_password}\n') - fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n') - fp.write(f'bind=0.0.0.0:{onionport}=onion\n') + writeTorSettings(fp, coin, coin_settings, tor_control_password) def make_rpc_func(bin_dir, data_dir, chain):