feature: Tor settings and tor_only mode (#765)

* Fix #634

* Fix #317

* Reorganize tor pannel

* Fix the test

* Specify block explorer explicitly on rescan utxo

* New Tor settings tab

* Tor settings admin only (also open HWI Bridge settings)

* Update tor.md

* Docs and small fixes

* delete leftover code
This commit is contained in:
benk10 2020-12-21 13:49:57 +02:00 committed by GitHub
parent 119a70e1e6
commit 5990f12f33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 479 additions and 287 deletions

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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)
)

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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.<br>It is therefore required that you set up authentication tab for Specter first to prevent unauthorized access.<br><br>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():

View file

@ -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":

View file

@ -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:

View file

@ -23,6 +23,7 @@
{% include "includes/qr-code.html" %}
{% endif %}
{% include "includes/message-box.html" %}
{% include "includes/tool-tip.html" %}
<div class="row holder">
{% block sidebar %}
{% include "includes/sidebar/sidebar.jinja" %}
@ -31,7 +32,6 @@
{% if current_user.is_authenticated and not hwi_bridge %}
<div class="row" id="status-bar" style="border-radius: 0 0 0 15px; position: absolute; right: 0; color: #ddd; background-color: #323e50;">
{% include "components/price_bar.jinja" %}
{% include "components/tor_address.jinja" %}
<a class="settings-bar-btn" href="{{ url_for('settings_endpoint.settings') }}">
<img src="{{ url_for('static', filename='img/settings-status-bar.svg') }}" style="width: 22px;"/>
</a>

View file

@ -1,154 +0,0 @@
<style>
#onion-address-container {
position: absolute;
width: 240px;
top: 10px;
padding: 10px;
margin: 10px;
background: #7D4698;
border-radius: 10px;
z-index: 1;
border: 1px solid #aaa;
}
#tor-btn {
position: absolute;
width: 24px;
margin-right: 50px;
z-index: 2;
border-radius: 10px;
}
#tor-toggle-btn {
cursor: pointer;
position: relative;
padding: 10px 40px 10px 20px;
}
.onion-address {
word-wrap: break-word;
color: #fff;
font-size: 0.8em;
text-decoration: none;
}
.onion-address:hover {
text-decoration: underline;
cursor: pointer;
}
.dot {
cursor: pointer;
height: 8px;
width: 8px;
position: absolute;
border-radius: 50%;
display: inline-block;
top: 9px;
left: 36px;
z-index: 3;
}
.dot-on {
{% if tor_enabled %}
background-color: #0f0;
{% else %}
background-color: #ccc;
{% endif %}
}
.dot-off {
{% if tor_enabled %}
background-color: #f00;
{% else %}
background-color: #ccc;
{% endif %}
}
.tor-btn {
margin-top: 10px;
width: 90px;
border: none;
font-size: 0.8em;
color: #fff;
padding: 5px;
border-radius: 5px;
border: 1px solid #59316B;
background: #7D4698;
}
.tor-btn:hover {
cursor: pointer;
background: #59316B;
}
</style>
<div id="onion-address-container" class="hidden optional">
<div class="tool-tip" style="margin: 3px;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info" style="margin-top: 22px;">
<span class="info">
<span class="info__title">Accessing Specter with a Tor hidden service<br></span><br>
Running a Tor hidden service with Specter allows you to securly access Specter from any remote device and location.<br><br>
<b>Please Note:</b><br>Connecting over Tor requires that you first configure Tor on your local machine.<br>
You can checkout <a target="_blank" href="https://www.keepitsimplebitcoin.com/how-to-install-tor/" style="color: #fff;">this video tutorial</a>, or read the documentation for <a target="_blank" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/tor.md" style="color: #fff;">Mac and Linux</a> or <a target="_blank" href="https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Tor/Tor.md#windows-10" style="color: #fff;">Window</a><br>
</span>
</p>
</div>
{% if tor_service_id %}
<span style="font-size: 0.95em; color: #fff;">Specter Running on Tor</span><br>
<div style="margin-top: 5px;"> <span style="font-size: 0.85em; color: #fff;">Running at:</span></div>
<div class="center">
<span title="Copy Tor address" class="onion-address centered" onclick="copyText('{{ tor_service_id }}.onion', 'Copied Tor hidden service address: {{ tor_service_id }}.onion')">
{{ tor_service_id }}.onion
</span>
</div>
{% if current_user.is_admin and (tor_service_id and tor_service_id + '.onion' not in request.url) %}
<form action="?" class="center">
<button class="tor-btn" type="submit" name="action" value="stoptor">Stop Tor</button>
</form>
{% endif %}
{% else %}
{% if debug %}
<span style="font-size: 0.95em; color: #fff;">Tor is unavailable...</span><br>
<div style="margin-top: 5px;">
<span style="font-size: 0.85em; color: #fff;">You are running Specter in debug mode, but Tor hidden services are only available in production mode.</span>
</div>
{% else %}
<span style="font-size: 0.95em; color: #fff;">Tor service is down...</span><br>
{% if current_user.is_admin and ('.onion' not in request.url) %}
<form action="?" class="center">
<button class="tor-btn" type="submit" name="action" value="starttor">Start Tor</button>
</form>
{% endif %}
{% endif %}
{% endif %}
</div>
<div class="settings-bar-btn" id='tor-toggle-btn'>
<img id="tor-btn" src="{{ url_for('static', filename='img/tor.svg') }}"/>
<span class="dot {% if tor_service_id %}dot-on{% else %}dot-off{% endif %}" id="tor-symbol-status-dot"></span>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
let torToggleBtn = document.getElementById('tor-toggle-btn');
let torDataContainer = document.getElementById('onion-address-container');
function toggleTorPopup() {
torDataContainer.style.display = torDataContainer.style.display == 'block' ? 'none' : 'block';
}
torToggleBtn.addEventListener("click", toggleTorPopup);
let right = 0;
for(let el of document.getElementsByClassName('settings-bar-btn')) {
if (el.id != 'price-bar-btn') {
right += el.offsetWidth;
}
}
torDataContainer.style.right = (right - 41) + 'px';
window.addEventListener('click', function(e){
if (!torDataContainer.contains(e.target) && !torToggleBtn.contains(e.target)){
if (torDataContainer.style.display == 'block') {
toggleTorPopup();
}
}
});
});
</script>

View file

@ -0,0 +1,101 @@
<template id="tooltip">
<style>
.tool-tip {
display: inline-block;
position: relative;
margin-left: 0.5em;
}
.tool-tip .tool-tip__icon {
color: #fff;
background: #27b1f0;
border-radius: 10px;
cursor: pointer;
display: inline-block;
font-style: italic;
font-family: times new roman;
height: 20px;
line-height: 1.3em;
text-align: center;
width: 20px;
}
.tool-tip .tool-tip__info {
display: none;
background: #262626;
border: 1px solid #27b1f0;
border-radius: 3px;
padding: 1em;
position: absolute;
left: 30px;
top: -20px;
width: 250px;
z-index: 2;
}
.tool-tip .tool-tip__info:before, .tool-tip .tool-tip__info:after {
content: "";
position: absolute;
left: -10px;
top: 7px;
border-style: solid;
border-width: 10px 10px 10px 0;
border-color: transparent #27b1f0;
}
.tool-tip .tool-tip__info:after {
left: -8px;
border-right-color: #262626;
}
.tool-tip .tool-tip__info .info {
display: block;
}
.tool-tip .tool-tip__info .info__title {
color: #4A90E2;
}
.tool-tip:hover .tool-tip__info, .tool-tip:focus .tool-tip__info {
display: inline-block;
}
a:focus + .tool-tip .tool-tip__info {
display: inline-block;
}
@media (hover: none) {
.tool-tip {
display: none;
}
}
</style>
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title"></span><br><br>
<slot></slot>
</span>
</p>
</div>
</template>
<script type="text/javascript">
class TooltipElement extends HTMLElement {
constructor() {
super();
// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});
var style = document.getElementById('tooltip').content;
var clone = style.cloneNode(true);
this.titleText = clone.querySelector(".info__title");
// Attach the created element to the shadow dom
shadow.appendChild(clone);
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(attrName, oldValue, newValue) {
let title = this.getAttribute('title');
this.titleText.innerHTML = title;
}
}
customElements.define('tool-tip', TooltipElement);
</script>

View file

@ -12,9 +12,10 @@
{{ settings_menu_item('bitcoin_core', 'Bitcoin Core', active_menuitem, isLeft=true) }}
{% endif %}
{{ settings_menu_item('general', 'General', active_menuitem, isLeft=(not current_user.is_admin)) }}
{{ settings_menu_item('auth', 'Authentication', active_menuitem, isRight=not current_user.is_admin) }}
{{ settings_menu_item('auth', 'Authentication', active_menuitem, isRight=false) }}
{{ settings_menu_item('hwi', 'HWI Bridge', active_menuitem, isRight=(not current_user.is_admin)) }}
{% if current_user.is_admin %}
{{ settings_menu_item('hwi', 'HWI Bridge', active_menuitem, isRight=true) }}
{{ settings_menu_item('tor', 'Tor', active_menuitem, isRight=true) }}
{% endif %}
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>

View file

@ -0,0 +1,76 @@
{% extends "base.jinja" %}
{% block main %}
<form action="?" method="POST" onsubmit="showPacman()">
<h1 id="title" class="settings-title">Tor settings - Specter Desktop {{ current_version }}</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('tor', current_user) }}
<div class="card" style="margin: 20px auto;">
<h1>
<img style="width: 25px; margin-right: 7px; vertical-align: middle;" src="{{ url_for('static', filename='img/tor.svg') }}"/><span style="vertical-align: bottom; margin-right: 10px;">
Tor configurations
</h1>
Use Tor proxy running at:
<input id="proxy-url" name="proxy_url" value="{{ proxy_url }}" type="url" placeholder="i.e. socks5://127.0.0.1:9050" />
<br>
<p class="note">
Default for Tor daemon: socks5://127.0.0.1:9050<br>
Default for Tor Browser: socks5://127.0.0.1:9150
</p>
Tor control port:
<p class="note">
Restart Specter for change of the control port to take effect.
</p>
<input name="tor_control_port" value="{{ tor_control_port }}" type="text" placeholder='Set to custom port here, if kept empty the default (9051 and 9151) is used' />
<br><br>
Do outside calls only over Tor?
<tool-tip title="Tor Only mode" style="float: right; margin-bottom: 5px;">
Some optional Specter functionalities, like rescanning UTXO on a pruned node, or getting the Bitcoin price, might make calls to external APIs and services.<br><br>
Toggle this on to ensure Specter routes all these external calls over Tor proxy.<br><br>
Note: Some external sources may stop working if they block Tor call.
</tool-tip>
<p class="note">
Toggle this on to ensure Specter routes all calls external over Tor proxy.
</p>
<div class="row">
<label class="switch">
<input type="checkbox" id="only-tor" name="only_tor" {% if only_tor %}checked{% endif %}>
<span class="slider"></span>
</label>
</div><br>
<div class="row">
<button type="submit" class="btn" name="action" value="test_tor">Test Tor Connection</button>&nbsp;
<button type="submit" class="btn" name="action" value="save">Save</button>
</div><br><br>
{% if not debug %}
<h2>Tor Hidden Service</h2>
<p>Running Specter behind a Tor hidden service allows you to acccess Specter over Tor from anywhere.<br>
</p>
{% if tor_service_id %}
{% if specter.config.auth == "none" %}
<p class="warning" style="max-width:700px;">
⚠️ Warning!<br>
Your are running Specter over Tor with no authentication settings configured.
This means your Specter instance is accessible to anyone with the .onion URL.
This .onion URL is publicly exposed and indexed on the Tor network - it is not secret!<br><br>
It is stronly adviced that you configure proper authentication while running Specter behind a Tor hidden service.
Please go to Settings -> Authentication and set up an authentication method.
</p>
{% endif %}
<p><b>
Specter hidden service is running at:
<span title="Copy Tor address" style="word-break: break-word;" class="explorer-link" onclick="copyText('http:/\/{{ tor_service_id }}.onion', 'Copied Tor hidden service address: {{ tor_service_id }}.onion')">
{{ tor_service_id }}.onion
</span>
</b></p>
{% endif %}
<div class="row">
<button type="submit" class="btn" name="action" value="toggle_hidden_service">
{% if tor_service_id %} Stop {% else %} Start {% endif %}
</button>
</div><br>
{% endif %}
</div>
</form>
{% endblock %}
{% block scripts %}
{% endblock %}

View file

@ -85,23 +85,22 @@
<div id="utxo-scan-options" style="display: none">
{% if specter.info['pruned'] %}
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> fetch missing data from block explorer
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Fetch missing data</span><br><br>
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
</span>
</p>
</div>
<tool-tip title="Fetch missing data">
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
</tool-tip>
</label>
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
<div id="utxo-explorer">
<br>
Block explorer URL:
<input id="explorer-url" name="explorer_url" value="{{ specter.get_default_explorer() }}" type="url" placeholder="Block explorer url" />
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
&#9432;<br>Careful with block explorers!<br>
This may have privacy implication!<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from the old (pruned) blocks.<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.<br>
"They" will know what transactions you are interested in.
</span>
</div>
{% endif %}
</div>
</div>
@ -238,7 +237,7 @@
let utxocheckbox = document.getElementById("use_explorer");
if(utxocheckbox){
utxocheckbox.addEventListener('change', e=>{
let el = document.getElementById("utxo-warning")
let el = document.getElementById("utxo-explorer")
if(utxocheckbox.checked){
el.style.display = "inline-block";
}else{

View file

@ -207,31 +207,29 @@
{% if specter.info['pruned'] %}
<div class="row" style="margin-top: 10px">
<label for="use_explorer"><input style="width: auto; min-width: auto;" type="checkbox" id="use_explorer" name="use_explorer" checked> fetch missing data from block explorer
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Fetch missing data</span><br><br>
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
</span>
</p>
</div>
<tool-tip title="Fetch missing data">
You are using a pruned node. Blocks for some transactions may be missing.<br><br>
Check this checkbox if you want to fetch missing information from block explorer.
</tool-tip>
</label>
</div>
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
&#9432;<br>Careful with block explorers!<br>
This may have privacy implication!<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.<br>
"They" will know what transactions you are interested in.
</span>
<div id="utxo-explorer">
<br>
Block explorer URL:
<input id="explorer-url" name="explorer_url" value="{{ specter.get_default_explorer() }}" type="url" placeholder="block explorer url" />
<span class="warning" id="utxo-warning" style="display: inline-block; max-width:300px;">
&#9432;<br>Careful with block explorers!<br>
This may have privacy implication!<br>
Information fetched from the block explorer contains a list of unspent transactions of your wallet from old (pruned) blocks.<br>
"They" will know what transactions you are interested in.
</span>
</div>
<script type="text/javascript">
let utxocheckbox = document.getElementById("use_explorer");
utxocheckbox.addEventListener('change', e=>{
let el = document.getElementById("utxo-warning")
let el = document.getElementById("utxo-explorer")
if(utxocheckbox.checked){
el.style.display = "inline-block";
el.style.display = "block";
}else{
el.style.display = "none";
}

View file

@ -21,7 +21,9 @@ def update_price(specter, current_user):
def get_price_at(specter, current_user, timestamp="now"):
try:
if specter.price_check:
requests_session = requests.Session()
requests_session = specter.requests_session(
force_tor=".onion/" in specter.price_provider
)
if specter.price_provider.startswith("bitstamp"):
currency = "usd"
currency_symbol = "$"
@ -59,8 +61,6 @@ def get_price_at(specter, current_user, timestamp="now"):
return False, 0, ""
return (True, price, "£")
if specter.price_provider.startswith("spotbit"):
requests_session.proxies["http"] = "socks5h://localhost:9050"
requests_session.proxies["https"] = "socks5h://localhost:9050"
currency = "usd"
currency_symbol = "$"
if specter.price_provider.endswith("_eur"):

View file

@ -40,6 +40,24 @@ def start_hidden_service(app):
f.write("%s.onion" % app.tor_service_id)
app.tor_service_id = app.tor_service_id
app.tor_enabled = True
if app.specter.config.get("auth", "none") == "none":
print(" * ############################# Warning! #############################")
print(
" * Your are running Specter over Tor with no authentication settings configured."
)
print(
" * This means your Specter instance is accessible to anyone with the .onion URL."
)
print(
" * This .onion URL is publicly exposed and indexed on the Tor network - it is not secret!"
)
print(
" * It is stronly adviced that you configure proper authentication while running Specter behind a Tor hidden service."
)
print(
" * Please go to Settings -> Authentication and set up an authentication method."
)
print(" * ####################################################################")
def stop_hidden_services(app):

View file

@ -12,12 +12,13 @@ logger = logging.getLogger(__name__)
class VersionChecker:
def __init__(self, name="cryptoadvance.specter"):
def __init__(self, name="cryptoadvance.specter", specter=None):
self.name = name
self.current = self.get_current_version()
self.latest = "unknown"
self.upgrade = False
self.running = False
self.specter = specter
def start(self):
if not self.running:
@ -72,7 +73,11 @@ class VersionChecker:
with open(version_file) as f:
current = f.read().strip()
try:
releases = requests.get(
if self.specter:
requests_session = self.specter.requests_session(force_tor=False)
else:
requests_session = requests.Session()
releases = requests_session.get(
"https://api.github.com/repos/cryptoadvance/specter-desktop/releases"
).json()
latest = "unknown"
@ -86,9 +91,13 @@ class VersionChecker:
return current, latest
def get_pip_version(self):
if self.specter:
requests_session = self.specter.requests_session(force_tor=False)
else:
requests_session = requests.Session()
try:
releases = (
requests.get("https://pypi.org/pypi/cryptoadvance.specter/json")
requests_session.get("https://pypi.org/pypi/cryptoadvance.specter/json")
.json()["releases"]
.keys()
)

View file

@ -674,35 +674,22 @@ class Wallet:
logging.error("Exception while processing txlist: {}".format(e))
return []
def full_txlist(self, validate_merkle_proofs=False):
tx_list = []
idx = 0
tx_len = 1
while tx_len > 0:
transactions = self.txlist(
idx, validate_merkle_proofs=validate_merkle_proofs
)
tx_list.append(transactions)
tx_len = len(transactions)
idx += 1
# Flatten the list
flat_list = []
for element in tx_list:
for dic_item in element:
flat_list.append(dic_item)
return flat_list
def gettransaction(self, txid, blockheight=None):
try:
return self._transactions.gettransaction(txid, blockheight)
except Exception as e:
logger.warning("Could not get transaction {}, error: {}".format(txid, e))
def rescanutxo(self, explorer=None):
def rescanutxo(self, explorer=None, requests_session=None):
delete_file(self._transactions.path)
self.fetch_transactions()
t = threading.Thread(target=self._rescan_utxo_thread, args=(explorer,))
t = threading.Thread(
target=self._rescan_utxo_thread,
args=(
explorer,
requests_session,
),
)
t.start()
def export_labels(self):
@ -721,7 +708,7 @@ class Wallet:
for address in addresses:
self._addresses.set_label(address, label)
def _rescan_utxo_thread(self, explorer=None):
def _rescan_utxo_thread(self, explorer=None, requests_session=None):
# rescan utxo is pretty fast,
# so we can check large range of addresses
# and adjust keypool accordingly
@ -796,13 +783,6 @@ class Wallet:
# handle missing transactions now
# if Tor is running, requests will be sent over Tor
if explorer is not None:
try:
requests_session = requests.Session()
requests_session.proxies["http"] = "socks5h://localhost:9050"
requests_session.proxies["https"] = "socks5h://localhost:9050"
requests_session.get(explorer)
except Exception:
requests_session = requests.Session()
# make sure there is no trailing /
explorer = explorer.rstrip("/")
try:

View file

@ -62,6 +62,9 @@ def test_settings_general_restore_wallet(bitcoin_regtest, caplog, client):
loglevel="debug",
restoredevices=restore_devices,
restorewallets=restore_wallets,
proxy_url="",
only_tor="off",
tor_control_port="",
),
)
assert b"Specter data was successfully loaded from backup." in result.data