From 92ebff592cb89b02713b192773cd5b630a6fcd63 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sat, 25 Jul 2020 00:06:34 +0300 Subject: [PATCH] Automatic Tor Hidden Service Support (#257) * Add Tor url to sidebar * Dynamic start/ stop Tor * Start Tor hidden service by default (if possible) * Updates * fixes * Update tor_address.jinja * Update tor_address.jinja * Allow starting Tor after running specter * Delete .env_example --- .env_example | 3 - docs/tor.md | 35 +---- requirements.txt | 3 +- src/cryptoadvance/specter/__main__.py | 126 +++++++++------ src/cryptoadvance/specter/controller.py | 29 +++- src/cryptoadvance/specter/static/img/tor.svg | 31 ++++ .../sidebar/components/tor_address.jinja | 144 ++++++++++++++++++ .../templates/includes/sidebar/sidebar.jinja | 1 + src/cryptoadvance/specter/tor_util.py | 87 ++++++----- tests/conftest.py | 2 + 10 files changed, 339 insertions(+), 122 deletions(-) delete mode 100644 .env_example create mode 100644 src/cryptoadvance/specter/static/img/tor.svg create mode 100644 src/cryptoadvance/specter/templates/includes/sidebar/components/tor_address.jinja diff --git a/.env_example b/.env_example deleted file mode 100644 index 94b2763f8..000000000 --- a/.env_example +++ /dev/null @@ -1,3 +0,0 @@ -# The cleartext password that was entered into: -# $ tor --hash-password "your-tor-passphrase" -TOR_PASSWORD=your-tor-passphrase diff --git a/docs/tor.md b/docs/tor.md index c528e989c..d52eb9fd4 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -14,23 +14,12 @@ Install Tor on the same server that you'll be running Specter Desktop: * [Debian / Ubuntu](https://2019.www.torproject.org/docs/debian.html.en) * [macOS](https://2019.www.torproject.org/docs/tor-doc-osx.html.en) -### Configure Tor authentication -```sh -$ tor --hash-password "your-tor-passphrase" -``` -That returns a password hash such as: -```sh -16:CE9058DA89498A4160373C70FF7FFF70CC2E20B6788FC48F5C35B2E85B -``` -Update your `torrc` config file (usually `/etc/tor/torrc` or `/usr/local/etc/tor/torrc` on macOS Homebrew installs). Uncomment the `ControlPort` line as well as the `HashedControlPassword` line. Remember to paste in your own hashed password result from above. +### Configure Tor port +Update your `torrc` config file (usually `/etc/tor/torrc` or `/usr/local/etc/tor/torrc` on macOS Homebrew installs) and uncomment the `ControlPort` line. ```sh ## The port on which Tor will listen for local connections from Tor ## controller applications, as documented in control-spec.txt. ControlPort 9051 -## If you enable the controlport, be sure to enable one of these -## authentication methods, to prevent attackers from accessing it. -HashedControlPassword 16:CE9058DA89498A4160373C70FF7FFF70CC2E20B6788FC48F5C35B2E85B -#CookieAuthentication 1 ``` Restart the Tor service: @@ -39,10 +28,10 @@ Restart the Tor service: ### Running with Tor using command line -You can start the server and provide your tor password using `--tor=` flag: +You can start the server using `--tor` flag: ```sh -$ python3 -m cryptoadvance.specter server --tor=your-tor-passphrase +$ python3 -m cryptoadvance.specter server --tor ``` ### Configure environment variables @@ -59,22 +48,6 @@ FLASK_ENV=production #FLASK_ENV=development ``` -### Specify Tor secrets - -The Tor password that we hashed above will need to be shared with Specter Desktop. - -Copy the example `.env_example` file: -```sh -$ cp .env_example .env -``` - -And then edit `.env` and specify `TOR_PASSWORD`: -```sh -# The cleartext password that was entered into: -# $ tor --hash-password "your-tor-passphrase" -TOR_PASSWORD=your-tor-passphrase -``` - ### Launch with Tor Now just start Specter Desktop as usual: diff --git a/requirements.txt b/requirements.txt index 252633587..ac524e8a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ certifi==2019.9.11 chardet==3.0.4 Click==7.0 daemonize==2.5.0 -ecdsa>=0.13.3 Flask==1.1.2 Flask-Cors==3.0.8 Flask-Login==0.5.0 @@ -12,7 +11,7 @@ pyserial==3.4 python-dotenv==0.13.0 requests==2.23.0 six==1.12.0 -stem==1.7.1 +stem==1.8.0 # only for testing currently docker==4.1.0 pytest==5.2.2 diff --git a/src/cryptoadvance/specter/__main__.py b/src/cryptoadvance/specter/__main__.py index 611ac0432..4dd87db5d 100644 --- a/src/cryptoadvance/specter/__main__.py +++ b/src/cryptoadvance/specter/__main__.py @@ -1,17 +1,16 @@ -import atexit import logging from logging.config import dictConfig import os import sys import time - +from stem.control import Controller +from . import tor_util import click import docker from .bitcoind import (BitcoindDockerController, fetch_wallet_addresses_for_mining) -from .helpers import which from .server import DATA_FOLDER, create_app, init_app from os import path @@ -30,15 +29,15 @@ def cli(): @click.option("--force", is_flag=True) # options below can help to run it on a remote server, # but better use nginx -@click.option("--port") # default - 25441 set to 80 for http, 443 for https -@click.option("--host", default="127.0.0.1") # set to 0.0.0.0 to make it available outside +@click.option("--port") # default - 25441 set to 80 for http, 443 for https +@click.option("--host", default="127.0.0.1") # set to 0.0.0.0 to make it available outside # for https: @click.option("--cert") @click.option("--key") -# provide tor password here -@click.option("--tor") +@click.option('--debug/--no-debug', default=None) +@click.option('--tor', is_flag=True) @click.option("--hwibridge", is_flag=True) -def server(daemon, stop, restart, force, port, host, cert, key, tor, hwibridge): +def server(daemon, stop, restart, force, port, host, cert, key, debug, tor, hwibridge): # we will store our daemon PID here pid_file = path.expanduser(path.join(DATA_FOLDER, "daemon.pid")) toraddr_file = path.expanduser(path.join(DATA_FOLDER, "onion.txt")) @@ -53,7 +52,7 @@ def server(daemon, stop, restart, force, port, host, cert, key, tor, hwibridge): time.sleep(0.3) try: os.remove(pid_file) - except Exception as e: + except Exception: pass elif daemon: if not force: @@ -82,7 +81,7 @@ def server(daemon, stop, restart, force, port, host, cert, key, tor, hwibridge): filename = os.path.join(dirname, filename) if os.path.isfile(filename): extra_files.append(filename) - + # if port is not defined - get it from environment if port is None: port = int(os.getenv('PORT', 25441)) @@ -108,53 +107,74 @@ def server(daemon, stop, restart, force, port, host, cert, key, tor, hwibridge): protocol = "https" if hwibridge: - app.logger.info("Running HWI Bridge mode, you can configure access to the API at: %s://%s:%d/hwi/settings" % (protocol, host, port)) - - # if tor password is not provided but env variable is set - if tor is None and os.getenv('CONNECT_TOR') == 'True': - from dotenv import load_dotenv - load_dotenv() # Load the secrets from .env - tor = os.getenv('TOR_PASSWORD') + app.logger.info( + "Running HWI Bridge mode, you can configure access \ + to the API at: %s://%s:%d/hwi/settings" + % (protocol, host, port) + ) # debug is false by default - def run(debug=False): - if tor is not None: - from . import tor_util + def run(debug=debug): + with Controller.from_port() as controller: + app.controller = controller + port = 5000 # default flask port + if 'port' in kwargs: + port = kwargs['port'] + else: + kwargs['port'] = port # if we have certificates if "ssl_context" in kwargs: tor_port = 443 else: tor_port = 80 - tor_util.run_on_hidden_service(app, - debug=False, - tor_password=tor, - tor_port=tor_port, - save_address_to=toraddr_file, - **kwargs) - else: + app.port = port + app.tor_port = tor_port + app.save_tor_address_to = toraddr_file + if debug and (tor or os.getenv('CONNECT_TOR') == 'True'): + print( + '* Warning: Cannot use Tor in debug mode. Starting in production mode instead.' + ) + if tor or os.getenv('CONNECT_TOR') == 'True': + try: + app.tor_enabled = True + tor_util.start_hidden_service(app) + except Exception as e: + print('* Failed to start Tor hidden service: {}'.format(e)) + print('* Continuing process with Tor disabled') + app.tor_service_id = None + app.tor_enabled = False + else: + app.tor_service_id = None + app.tor_enabled = False app.run(debug=debug, **kwargs) + tor_util.stop_hidden_services(app) # check if we should run a daemon or not if daemon or restart: print("Starting server in background...") print("* Hopefully running on %s://%s:%d/" % (protocol, host, port)) - if tor is not None: - print("* For onion address check the file %s" % toraddr_file) # macOS + python3.7 is buggy - if sys.platform=="darwin" and (sys.version_info.major==3 and sys.version_info.minor < 8): - print("* WARNING: --daemon mode might not work properly in python 3.7 and lower on MacOS. Upgrade to python 3.8+") + if sys.platform == "darwin" and \ + (sys.version_info.major == 3 and sys.version_info.minor < 8): + print( + "* WARNING: --daemon mode might not work properly in python 3.7 \ + and lower on MacOS. Upgrade to python 3.8+" + ) from daemonize import Daemonize d = Daemonize(app="specter", pid=pid_file, action=run) d.start() else: # if not a daemon we can use DEBUG - run(app.config['DEBUG']) + if debug is None: + debug = app.config['DEBUG'] + run(debug=debug) + @cli.command() @click.option('--debug/--no-debug', default=False) @click.option('--mining/--no-mining', default=True) @click.option('--docker-tag', "docker_tag", default="latest") -def bitcoind(debug,mining, docker_tag): +def bitcoind(debug, mining, docker_tag): mining_every_x_seconds = 15 if debug: logging.getLogger().setLevel(logging.DEBUG) @@ -164,20 +184,33 @@ def bitcoind(debug,mining, docker_tag): my_bitcoind.start_bitcoind() except docker.errors.ImageNotFound: click.echo(" --> Image with tag {} does not exist!".format(docker_tag)) - click.echo(" --> Try to download first with docker pull registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format(docker_tag)) + click.echo( + " --> Try to download first with docker pull \ + registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}" + .format(docker_tag) + ) sys.exit(1) - tags_of_image = [ image.split(":")[-1] for image in my_bitcoind.btcd_container.image.tags] - if not docker_tag in tags_of_image: + tags_of_image = [image.split(":")[-1] for image in my_bitcoind.btcd_container.image.tags] + if docker_tag not in tags_of_image: click.echo(" --> The running docker container is not the tag you requested!") - click.echo(" --> please stop first with docker stop {}".format(my_bitcoind.btcd_container.id)) + click.echo( + " --> please stop first with docker stop {}" + .format(my_bitcoind.btcd_container.id) + ) sys.exit(1) click.echo(" --> containerImage: %s" % my_bitcoind.btcd_container.image.tags) click.echo(" --> url: %s" % my_bitcoind.rpcconn.render_url()) click.echo(" --> user, password: bitcoin, secret") click.echo(" --> host, port: localhost, 18443") - click.echo(" --> bitcoin-cli: bitcoin-cli -regtest -rpcuser=bitcoin -rpcpassword=secret getblockchaininfo ") + click.echo( + " --> bitcoin-cli: bitcoin-cli -regtest -rpcuser=bitcoin \ + -rpcpassword=secret getblockchaininfo " + ) if mining: - click.echo(" --> Now, mining a block every %i seconds. Avoid it via --no-mining" % mining_every_x_seconds) + click.echo( + " --> Now, mining a block every %i seconds. Avoid it via --no-mining" % + mining_every_x_seconds + ) # Get each address some coins try: for address in fetch_wallet_addresses_for_mining(): @@ -188,22 +221,21 @@ def bitcoind(debug,mining, docker_tag): # make them spendable my_bitcoind.mine(block_count=100) - click.echo(" --> ",nl=False) + click.echo(" --> ", nl=False) i = 0 while True: my_bitcoind.mine() - click.echo("%i"% (i%10),nl=False) - if i%10 == 9: - click.echo(" ",nl=False) + click.echo("%i" % (i % 10), nl=False) + if i % 10 == 9: + click.echo(" ", nl=False) i += 1 if i >= 50: - i=0 + i = 0 click.echo(" ") - click.echo(" --> ",nl=False) + click.echo(" --> ", nl=False) time.sleep(mining_every_x_seconds) - if __name__ == "__main__": # central and early configuring of logging # see https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration @@ -222,4 +254,4 @@ if __name__ == "__main__": 'handlers': ['wsgi'] } }) - cli() \ No newline at end of file + cli() diff --git a/src/cryptoadvance/specter/controller.py b/src/cryptoadvance/specter/controller.py index 6e7c7e991..274e76dd7 100644 --- a/src/cryptoadvance/specter/controller.py +++ b/src/cryptoadvance/specter/controller.py @@ -30,6 +30,8 @@ from io import BytesIO import traceback from .devices.electrum import b43_decode from binascii import b2a_base64 +from .tor_util import start_hidden_service, stop_hidden_services +from stem.control import Controller from pathlib import Path env_path = Path('.') / '.flaskenv' @@ -55,12 +57,31 @@ def selfcheck(): if app.config.get('LOGIN_DISABLED'): app.login('admin') + ########## template injections ############# @app.context_processor def inject_debug(): ''' Can be used in all jinja2 templates ''' return dict(debug=app.config['DEBUG']) + +@app.context_processor +def inject_tor(): + if app.config['DEBUG']: + return dict(tor_service_id='', tor_enabled=False) + if request.args.get('action', '') == 'stoptor' or request.args.get('action', '') == 'starttor': + if hasattr(current_user, 'is_admin') and current_user.is_admin: + try: + current_hidden_services = app.controller.list_ephemeral_hidden_services() + except Exception: + current_hidden_services = [] + if request.args.get('action', '') == 'stoptor' and len(current_hidden_services) != 0: + stop_hidden_services(app) + if request.args.get('action', '') == 'starttor' and len(current_hidden_services) == 0: + start_hidden_service(app) + return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled) + + ################ routes #################### @app.route('/wallets//combine/', methods=['GET', 'POST']) @login_required @@ -282,10 +303,10 @@ def general_settings(): hwi_bridge_url = request.form['hwi_bridge_url'] if current_user.is_admin: loglevel = request.form['loglevel'] - + if action == "save": if current_user.is_admin: - set_loglevel(app,loglevel) + set_loglevel(app, loglevel) app.specter.update_explorer(explorer, current_user) app.specter.update_hwi_bridge_url(hwi_bridge_url, current_user) @@ -335,7 +356,7 @@ def bitcoin_core_settings(): passwd = request.form['password'] port = request.form['port'] host = request.form['host'] - + # protocol://host if "://" in host: arr = host.split("://") @@ -706,7 +727,7 @@ def new_wallet(wallet_type): err = "%r" % e wallet.getdata() return redirect("/wallets/%s/" % wallet.alias) - + return render_template( "wallet/new_wallet/new_wallet.jinja", wallet_type=wallet_type, diff --git a/src/cryptoadvance/specter/static/img/tor.svg b/src/cryptoadvance/specter/static/img/tor.svg new file mode 100644 index 000000000..2acd05b82 --- /dev/null +++ b/src/cryptoadvance/specter/static/img/tor.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/components/tor_address.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/components/tor_address.jinja new file mode 100644 index 000000000..c72c539a2 --- /dev/null +++ b/src/cryptoadvance/specter/templates/includes/sidebar/components/tor_address.jinja @@ -0,0 +1,144 @@ + +
+ + +
+ + \ No newline at end of file diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja index 40f033aa0..67ba12c32 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja @@ -1,6 +1,7 @@ {% from 'includes/sidebar/components/sidebar_btn.jinja' import sidebar_btn %}