diff --git a/docs/tor.md b/docs/tor.md
index 9e8bcda28..020174e52 100644
--- a/docs/tor.md
+++ b/docs/tor.md
@@ -12,8 +12,12 @@ Make sure authentication is enabled to avoid access to your Specter by random st
### Security note
Tor support, like Specter Desktop as a whole, should be treated as a work-in-progress that is not yet vetted as being fully secure.
+### Setting up Tor using the Tor Browser
+The easiest way to use Tor with Specter is to have the [Tor Browser](https://www.torproject.org/download/) open. Just install it on your computer, open it and it will expose a Tor proxy Specter can connect to.
+To set this up, you'll just need to go to Specter's Settings -> Tor tab, then set the Tor Proxy field to: `socks5://127.0.0.1:9150` (you'll see it listed below the field as the Tor Browser default URL). Then click save and Specter should be working with Tor - just make sure to keep your Tor Browser running while using Specter.
+
### Install Tor service
-Install Tor on the same server that you'll be running Specter Desktop:
+Alternatively, you can setup the Tor service. 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)
diff --git a/src/cryptoadvance/specter/cli/cli_server.py b/src/cryptoadvance/specter/cli/cli_server.py
index 730a98dba..6fe65b8c0 100644
--- a/src/cryptoadvance/specter/cli/cli_server.py
+++ b/src/cryptoadvance/specter/cli/cli_server.py
@@ -9,6 +9,7 @@ from socket import gethostname
import click
from OpenSSL import SSL, crypto
from stem.control import Controller
+from urllib.parse import urlparse
from ..server import create_app, init_app
from ..util.tor import start_hidden_service, stop_hidden_services
@@ -195,7 +196,15 @@ def server(
# debug is false by default
def run(debug=debug):
try:
- app.controller = Controller.from_port()
+ tor_control_address = urlparse(app.specter.proxy_url).netloc.split(":")[0]
+ if tor_control_address == "localhost":
+ tor_control_address = "127.0.0.1"
+ app.controller = Controller.from_port(
+ address=tor_control_address,
+ port=int(app.specter.tor_control_port)
+ if app.specter.tor_control_port
+ else "default",
+ )
except Exception:
app.controller = None
try:
diff --git a/src/cryptoadvance/specter/helpers.py b/src/cryptoadvance/specter/helpers.py
index 72b39f48a..ce25ac8ea 100644
--- a/src/cryptoadvance/specter/helpers.py
+++ b/src/cryptoadvance/specter/helpers.py
@@ -296,3 +296,20 @@ def notify_upgrade(app, flash):
"info",
)
return app.specter.version.current
+
+
+def is_ip_private(ip):
+ # https://en.wikipedia.org/wiki/Private_network
+ priv_lo = re.compile("^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+ priv_24 = re.compile("^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+ priv_20 = re.compile("^192\.168\.\d{1,3}.\d{1,3}$")
+ priv_16 = re.compile("^172.(1[6-9]|2[0-9]|3[0-1]).[0-9]{1,3}.[0-9]{1,3}$")
+
+ res = (
+ ip == "localhost"
+ or priv_lo.match(ip)
+ or priv_24.match(ip)
+ or priv_20.match(ip)
+ or priv_16.match(ip)
+ )
+ return res is not None
diff --git a/src/cryptoadvance/specter/hwi_server.py b/src/cryptoadvance/specter/hwi_server.py
index bfc969215..68ebd87f7 100644
--- a/src/cryptoadvance/specter/hwi_server.py
+++ b/src/cryptoadvance/specter/hwi_server.py
@@ -93,12 +93,10 @@ def api():
500,
)
data["forwarded_request"] = True
- requests_session = requests.Session()
+ requests_session = app.specter.requests_session(
+ force_tor=".onion/" in app.specter.hwi_bridge_url
+ )
requests_session.headers.update({"origin": request.environ["HTTP_ORIGIN"]})
- if ".onion/" in app.specter.hwi_bridge_url:
- requests_session.proxies = {}
- requests_session.proxies["http"] = "socks5h://localhost:9050"
- requests_session.proxies["https"] = "socks5h://localhost:9050"
forwarded_request = requests_session.post(
app.specter.hwi_bridge_url, data=json.dumps(data)
)
diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py
index 7b2d5e339..3c667dd56 100644
--- a/src/cryptoadvance/specter/rpc.py
+++ b/src/cryptoadvance/specter/rpc.py
@@ -1,6 +1,7 @@
import logging
import requests, json, os
import os, sys, errno
+from .helpers import is_ip_private
logger = logging.getLogger(__name__)
@@ -137,7 +138,12 @@ def detect_rpc_confs_via_env():
return rpc_arr
-def autodetect_rpc_confs(datadir=get_default_datadir(), port=None):
+def autodetect_rpc_confs(
+ datadir=get_default_datadir(),
+ port=None,
+ proxy_url="socks5h://localhost:9050",
+ only_tor=False,
+):
"""Returns an array of valid and working configurations which
got autodetected.
autodetection checks env-vars and bitcoin-data-dirs
@@ -152,7 +158,9 @@ def autodetect_rpc_confs(datadir=get_default_datadir(), port=None):
available_conf_arr = []
if len(conf_arr) > 0:
for conf in conf_arr:
- rpc = BitcoinRPC(**conf)
+ rpc = BitcoinRPC(
+ **conf, proxy_url="socks5h://localhost:9050", only_tor=False
+ )
if port is not None:
if int(rpc.port) != port:
continue
@@ -209,6 +217,8 @@ class BitcoinRPC:
path="",
timeout=None,
session=None,
+ proxy_url="socks5h://localhost:9050",
+ only_tor=False,
**kwargs,
):
path = path.replace("//", "/") # just in case
@@ -219,15 +229,18 @@ class BitcoinRPC:
self.host = host
self.path = path
self.timeout = timeout
+ self.proxy_url = (proxy_url,)
+ self.only_tor = (only_tor,)
self.r = None
# session reuse speeds up requests
if session is None:
session = requests.Session()
# check if we need to connect over Tor
- if ".onion" in self.host:
- # configure Tor proxies
- session.proxies["http"] = "socks5h://localhost:9050"
- session.proxies["https"] = "socks5h://localhost:9050"
+ if not is_ip_private(host):
+ if only_tor or ".onion" in self.host:
+ # configure Tor proxies
+ session.proxies["http"] = proxy_url
+ session.proxies["https"] = proxy_url
self.session = session
def wallet(self, name=""):
@@ -240,6 +253,8 @@ class BitcoinRPC:
path="{}/wallet/{}".format(self.path, name),
timeout=self.timeout,
session=self.session,
+ proxy_url=self.proxy_url,
+ only_tor=self.only_tor,
)
@property
@@ -270,6 +285,8 @@ class BitcoinRPC:
self.path,
self.timeout,
self.session,
+ self.proxy_url,
+ self.only_tor,
)
def multi(self, calls: list, **kwargs):
diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py
index e10350ea5..af9b6a988 100644
--- a/src/cryptoadvance/specter/server.py
+++ b/src/cryptoadvance/specter/server.py
@@ -59,7 +59,7 @@ def init_app(app, hwibridge=False, specter=None):
# version checker
# checks for new versions once per hour
- specter.version = VersionChecker()
+ specter.version = VersionChecker(specter=specter)
specter.version.start()
login_manager = LoginManager()
diff --git a/src/cryptoadvance/specter/server_endpoints/controller.py b/src/cryptoadvance/specter/server_endpoints/controller.py
index 901f8e25f..fb1c7b375 100644
--- a/src/cryptoadvance/specter/server_endpoints/controller.py
+++ b/src/cryptoadvance/specter/server_endpoints/controller.py
@@ -1,4 +1,4 @@
-import random, traceback
+import random, traceback, socket
from datetime import datetime
from flask import (
@@ -74,45 +74,6 @@ def inject_debug():
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
- ):
- try:
- start_hidden_service(app)
- except Exception as e:
- flash(
- "Failed to start Tor hidden service.\
-Make sure you have Tor running with ControlPort configured and try again.\
-Error returned: {}".format(
- e
- ),
- "error",
- )
- return dict(tor_service_id="", tor_enabled=False)
- return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled)
-
-
################ Specter global routes ####################
@app.route("/")
@login_required
diff --git a/src/cryptoadvance/specter/server_endpoints/settings.py b/src/cryptoadvance/specter/server_endpoints/settings.py
index ea0e8bb87..141766e54 100644
--- a/src/cryptoadvance/specter/server_endpoints/settings.py
+++ b/src/cryptoadvance/specter/server_endpoints/settings.py
@@ -22,6 +22,7 @@ from ..helpers import (
)
from ..persistence import write_devices, write_wallet
from ..user import hash_password
+from ..util.tor import start_hidden_service, stop_hidden_services
rand = random.randint(0, 1e32) # to force style refresh
@@ -233,6 +234,97 @@ This may take a few hours to complete.",
)
+@settings_endpoint.route("/tor", methods=["GET", "POST"])
+@login_required
+def tor():
+ """
+ controls the tor related settings
+ GET for displaying the page, POST for updates
+ param action might be "save", "test_tor" or "toggle_hidden_service"
+ param proxy_url the Tor deamon url, usually something like socks5h://localhost:9050
+ param only_tor "on" or something else ("off")
+ """
+ if not current_user.is_admin:
+ flash("Only an admin is allowed to access this page.", "error")
+ return redirect("")
+ current_version = notify_upgrade(app, flash)
+ proxy_url = app.specter.proxy_url
+ only_tor = app.specter.only_tor
+ tor_control_port = app.specter.tor_control_port
+ if request.method == "POST":
+ action = request.form["action"]
+ proxy_url = request.form["proxy_url"]
+ only_tor = request.form.get("only_tor") == "on"
+ tor_control_port = request.form["tor_control_port"]
+
+ if action == "save":
+ app.specter.update_proxy_url(proxy_url, current_user)
+ app.specter.update_only_tor(only_tor, current_user)
+ app.specter.update_tor_control_port(tor_control_port, current_user)
+ app.specter.check()
+ elif action == "test_tor":
+ try:
+ requests_session = requests.Session()
+ requests_session.proxies["http"] = proxy_url
+ requests_session.proxies["https"] = proxy_url
+ res = requests_session.get(
+ "http://expyuzz4wqqyqhjn.onion", # Tor Project onion website
+ )
+ tor_connectable = res.status_code == 200
+ if tor_connectable:
+ flash("Tor requests test completed successfully!", "info")
+ else:
+ flash("Failed to make test request over Tor.", "error")
+ except Exception as e:
+ flash("Failed to make test request over Tor. Error: %s" % e, "error")
+ tor_connectable = False
+ elif action == "toggle_hidden_service":
+ if not app.config["DEBUG"]:
+ if app.specter.config.get("auth", "none") == "none":
+ flash(
+ "Enabling Tor hidden service will expose your Specter for remote access.
It is therefore required that you set up authentication tab for Specter first to prevent unauthorized access.
Please go to Settings -> Authentication and set up an authentication method and retry.",
+ "error",
+ )
+ else:
+ 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 len(current_hidden_services) != 0:
+ stop_hidden_services(app)
+ flash("Tor hidden service turn off successfully", "info")
+ else:
+ try:
+ start_hidden_service(app)
+ flash("Tor hidden service turn on successfully", "info")
+ except Exception as e:
+ flash(
+ "Failed to start Tor hidden service. Make sure you have Tor running with ControlPort configured and try again. Error returned: {}".format(
+ e
+ ),
+ "error",
+ )
+ else:
+ flash(
+ "Can't toggle hidden service while Specter is running in DEBUG mode",
+ "error",
+ )
+
+ return render_template(
+ "settings/tor_settings.jinja",
+ proxy_url=proxy_url,
+ only_tor=only_tor,
+ tor_control_port=tor_control_port,
+ tor_service_id=app.tor_service_id,
+ specter=app.specter,
+ current_version=current_version,
+ rand=rand,
+ )
+
+
@settings_endpoint.route("/auth", methods=["GET", "POST"])
@login_required
def auth():
diff --git a/src/cryptoadvance/specter/server_endpoints/wallets.py b/src/cryptoadvance/specter/server_endpoints/wallets.py
index 9d1ad010a..4a537c7ca 100644
--- a/src/cryptoadvance/specter/server_endpoints/wallets.py
+++ b/src/cryptoadvance/specter/server_endpoints/wallets.py
@@ -370,8 +370,8 @@ def new_wallet(wallet_type):
if "utxo" in request.form.get("full_rescan_option"):
explorer = None
if "use_explorer" in request.form:
- explorer = app.specter.get_default_explorer()
- wallet.rescanutxo(explorer)
+ explorer = request.form["explorer_url"]
+ wallet.rescanutxo(explorer, app.specter.requests_session(explorer))
app.specter.info["utxorescan"] = 1
app.specter.utxorescanwallet = wallet.alias
else:
@@ -793,8 +793,8 @@ def settings(wallet_alias):
elif action == "rescanutxo":
explorer = None
if "use_explorer" in request.form:
- explorer = app.specter.get_default_explorer()
- wallet.rescanutxo(explorer)
+ explorer = request.form["explorer_url"]
+ wallet.rescanutxo(explorer, app.specter.requests_session(explorer))
app.specter.info["utxorescan"] = 1
app.specter.utxorescanwallet = wallet.alias
elif action == "abortrescanutxo":
diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py
index 950c75053..d6c119ddb 100644
--- a/src/cryptoadvance/specter/specter.py
+++ b/src/cryptoadvance/specter/specter.py
@@ -6,6 +6,7 @@ import traceback
import random
import time
import zipfile
+import requests
from io import BytesIO
from .helpers import deep_update, clean_psbt, is_testnet
from .util.checker import Checker
@@ -24,7 +25,13 @@ import threading
logger = logging.getLogger(__name__)
-def get_rpc(conf, old_rpc=None, return_broken_instead_none=False):
+def get_rpc(
+ conf,
+ old_rpc=None,
+ return_broken_instead_none=False,
+ proxy_url="socks5h://localhost:9050",
+ only_tor=False,
+):
"""
Checks if config have changed, compares with old rpc
and returns new one if necessary
@@ -44,7 +51,7 @@ def get_rpc(conf, old_rpc=None, return_broken_instead_none=False):
datadir=os.path.expanduser(conf["datadir"])
)
if len(rpc_conf_arr) > 0:
- rpc = BitcoinRPC(**rpc_conf_arr[0])
+ rpc = BitcoinRPC(**rpc_conf_arr[0], proxy_url=proxy_url, only_tor=only_tor)
else:
# if autodetect is disabled and port is not defined
# we use default port 8332
@@ -109,6 +116,9 @@ class Specter:
},
"auth": "none",
"explorers": {"main": "", "test": "", "regtest": "", "signet": ""},
+ "proxy_url": "socks5h://localhost:9050", # Tor proxy URL
+ "only_tor": False,
+ "tor_control_port": "",
"hwi_bridge_url": "/hwi/api/",
# unique id that will be used in wallets path in Bitcoin Core
# empty by default for backward-compatibility
@@ -150,7 +160,12 @@ class Specter:
# update rpc if something doesn't work
rpc = self.rpc
if rpc is None or not rpc.test_connection():
- rpc = get_rpc(self.config["rpc"], self.rpc)
+ rpc = get_rpc(
+ self.config["rpc"],
+ self.rpc,
+ proxy_url=self.proxy_url,
+ only_tor=self.only_tor,
+ )
# if rpc is not available
# do checks more often, once in 20 seconds
@@ -334,7 +349,13 @@ class Specter:
def test_rpc(self, **kwargs):
conf = copy.deepcopy(self.config["rpc"])
conf.update(kwargs)
- rpc = get_rpc(conf, return_broken_instead_none=True)
+
+ rpc = get_rpc(
+ conf,
+ return_broken_instead_none=True,
+ proxy_url=self.proxy_url,
+ only_tor=self.only_tor,
+ )
if rpc is None:
return {"out": "", "err": "autodetect failed", "code": -1}
r = {}
@@ -399,7 +420,12 @@ class Specter:
self.config["rpc"][k] = kwargs[k]
need_update = True
if need_update:
- self.rpc = get_rpc(self.config["rpc"], None)
+ self.rpc = get_rpc(
+ self.config["rpc"],
+ None,
+ proxy_url=self.proxy_url,
+ only_tor=self.only_tor,
+ )
self._save()
self.check(check_all=True)
return self.rpc is not None
@@ -427,6 +453,24 @@ class Specter:
else:
user.set_explorer(self, explorer)
+ def update_proxy_url(self, proxy_url, user):
+ """ update the Tor proxy url """
+ if self.config["proxy_url"] != proxy_url:
+ self.config["proxy_url"] = proxy_url
+ self._save()
+
+ def update_only_tor(self, only_tor, user):
+ """ switch whatever to use Tor for all calls """
+ if self.config["only_tor"] != only_tor:
+ self.config["only_tor"] = only_tor
+ self._save()
+
+ def update_tor_control_port(self, tor_control_port, user):
+ """ set the control port of the tor daemon """
+ if self.config["tor_control_port"] != tor_control_port:
+ self.config["tor_control_port"] = tor_control_port
+ self._save()
+
def update_hwi_bridge_url(self, url, user):
""" update the hwi bridge url to use """
user = self.user_manager.get_user(user)
@@ -587,6 +631,18 @@ class Specter:
def explorer(self):
return self.user_config.get("explorers", {}).get(self.chain, "")
+ @property
+ def proxy_url(self):
+ return self.user_config.get("proxy_url", "socks5h://localhost:9050")
+
+ @property
+ def only_tor(self):
+ return self.user_config.get("only_tor", False)
+
+ @property
+ def tor_control_port(self):
+ return self.user_config.get("tor_control_port", "")
+
@property
def hwi_bridge_url(self):
return self.user_config.get("hwi_bridge_url", "")
@@ -629,6 +685,13 @@ class Specter:
def wallet_manager(self):
return self.user.wallet_manager
+ def requests_session(self, force_tor=False):
+ requests_session = requests.Session()
+ if self.only_tor or force_tor:
+ requests_session.proxies["http"] = self.proxy_url
+ requests_session.proxies["https"] = self.proxy_url
+ return requests_session
+
def specter_backup_file(self):
memory_file = BytesIO()
with zipfile.ZipFile(memory_file, "w") as zf:
diff --git a/src/cryptoadvance/specter/templates/base.jinja b/src/cryptoadvance/specter/templates/base.jinja
index 6a01b541e..cbc92070d 100644
--- a/src/cryptoadvance/specter/templates/base.jinja
+++ b/src/cryptoadvance/specter/templates/base.jinja
@@ -23,6 +23,7 @@
{% include "includes/qr-code.html" %}
{% endif %}
{% include "includes/message-box.html" %}
+ {% include "includes/tool-tip.html" %}
-
- Accessing Specter with a Tor hidden service
- Running a Tor hidden service with Specter allows you to securly access Specter from any remote device and location.
- Please Note:
Connecting over Tor requires that you first configure Tor on your local machine.
- You can checkout this video tutorial, or read the documentation for Mac and Linux or Window
-
-
+
+
+