mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
command-line options for server: daemon, ssl certificates, tor and others (#70)
* --daemon and --stop options for server * --restart and --force option * ssl certificates and tor * tor key path fix... * pass host to flask * fix bug with switching between nodes * docs on node setup - reverse proxy and self-signed certs * typo
This commit is contained in:
parent
556ec5f961
commit
9039bc2c4d
9 changed files with 358 additions and 35 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ build
|
|||
dist
|
||||
*.egg-info
|
||||
.vscode
|
||||
cert.pem
|
||||
key.pem
|
||||
22
README.md
22
README.md
|
|
@ -24,7 +24,7 @@ HWI support requires `libusb` (necessary? Or is `pip install libusb1` sufficient
|
|||
* Ubuntu/Debian: `sudo apt install libusb-1.0-0-dev libudev-dev`
|
||||
* macOS: `brew install libusb`
|
||||
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/cryptoadvance/specter-desktop.git
|
||||
cd specter-desktop
|
||||
virtualenv --python=python3 .env
|
||||
|
|
@ -35,15 +35,29 @@ pip3 install -e .
|
|||
|
||||
Run the server:
|
||||
|
||||
```
|
||||
```sh
|
||||
cd specter-desktop
|
||||
python3 -m cryptoadvance.specter server
|
||||
```
|
||||
|
||||
You can also run it as a daemon, using tor, provide ssl certificates to run over https. Https is especially important because browsers don't allow the website to access camera without secure connection, and we need camera access to scan QR codes.
|
||||
|
||||
An example how to run specter server in the background (`--daemon`) with ssl certificates (`--key`, `--cert`) over tor:
|
||||
|
||||
```sh
|
||||
python -m cryptoadvance.specter server --tor=mytorpassword --cert=./cert.pem --key=./key.pem --daemon
|
||||
```
|
||||
|
||||
If your Bitcoin Core is using a default data folder the app should detect it automatically. If not, consider setting `rpcuser` and `rpcpassword` in the `bitcoin.conf` file and in the app settings.
|
||||
|
||||
Have a look at [DEVELOPMENT.md](https://github.com/cryptoadvance/specter-desktop/blob/master/DEVELOPMENT.md) for further information about hacking on specter-desktop.
|
||||
|
||||
## Detailed instructions
|
||||
|
||||
- Beyond local network - how to forward your node through a cheap VPS: [docs/reverse-proxy.md](docs/reverse-proxy.md)
|
||||
- Setting up Specter over Tor: [docs/tor.md](docs/tor.md)
|
||||
- Using self-signed certificates in local network or Tor: [docs/self-signed-certificates.md](docs/self-signed-certificates.md)
|
||||
|
||||
## A few screenshots
|
||||
|
||||
### Adding a new device
|
||||
|
|
@ -69,7 +83,3 @@ Have a look at [DEVELOPMENT.md](https://github.com/cryptoadvance/specter-desktop
|
|||
### Configuration
|
||||
|
||||

|
||||
|
||||
|
||||
## Make Specter Desktop available externally over Tor
|
||||
see: [docs/tor.md](docs/tor.md)
|
||||
|
|
|
|||
118
docs/reverse-proxy.md
Normal file
118
docs/reverse-proxy.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Specter over SSH-tunnel
|
||||
|
||||
If you want to have access to your wallet outside of your local network you can either [use Tor](./tor.md), or make a reverse proxy from your node to a cheap VPS somewhere.
|
||||
|
||||
Here we will describe how to set up your VPS server to forward all requests to your Bitcoin node.
|
||||
|
||||
You can either have both Specter and Bitcoin Core on the same node and forward Specter interface to remote server, or you can only do it for Bitcoin Core and keep Specter on your laptop. I will assume first option, if you want to go with the second one just change the port from `25441` to `8334` or whatever port your Core is using.
|
||||
|
||||
## Basic configuration
|
||||
|
||||
Update your remote server:
|
||||
|
||||
```sh
|
||||
apt update && apt upgrade
|
||||
```
|
||||
|
||||
Install nginx:
|
||||
|
||||
```sh
|
||||
apt install nginx
|
||||
```
|
||||
|
||||
Add a new server to your nginx configuration `/etc/nginx/sites-enabled/default`:
|
||||
|
||||
```sh
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
# optinaly configure domain name
|
||||
server_name specter.mydomain.com;
|
||||
# set proxy pass
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:25441;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart nginx:
|
||||
|
||||
```sh
|
||||
nginx -s reload
|
||||
```
|
||||
|
||||
On your local computer (where Specter is running) start a reverse proxy:
|
||||
|
||||
```sh
|
||||
ssh -nN -R 25441:localhost:25441 user@specter.mydomain.com
|
||||
```
|
||||
|
||||
Check in your browser - when you navigate to `http://specter.mydomain.com` you should see Specter already.
|
||||
|
||||
## Adding HTTPS
|
||||
|
||||
HTTPS is very important, not only because it is secure, but also because without HTTPS we can't use camera to scan QR codes. We need to get secure connection.
|
||||
|
||||
Letsencrypt issues certificates for free, they just need to check that you control the domain. So it doesn't work for local network or for Tor addresses. In these cases you would need to create a [self-signed certificate](./self-signed-certificates.md). But domains are cheap and probably everyone has a dozen that is not used. So we assume you have one.
|
||||
|
||||
Install certbot to issue certificate for us:
|
||||
|
||||
```sh
|
||||
apt install software-properties-common
|
||||
add-apt-repository universe
|
||||
add-apt-repository ppa:certbot/certbot
|
||||
apt update
|
||||
apt install certbot python-certbot-nginx
|
||||
```
|
||||
|
||||
Run `certbot` and answer it's questions:
|
||||
|
||||
```sh
|
||||
certbot --nginx
|
||||
```
|
||||
|
||||
Now Specter should be available over HTTPS: `https://specter.mydomain.com`
|
||||
|
||||
## Adding basic authentication
|
||||
|
||||
*Note:* This part is only required if you are running Specter on your node. If you are exposing your Bitcoin RPC it already has authentication with rpcuser and rpcpassword.
|
||||
|
||||
We don't want random people to have access to our wallet, so we want to protect it with login and password.
|
||||
|
||||
Nginx has a nice [documentation](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/) on the topic, but I will copy-paste main commands here.
|
||||
|
||||
Verify that `apache2-utils` (Debian, Ubuntu) or `httpd-tools` (RHEL/CentOS/Oracle Linux) is installed.
|
||||
|
||||
```sh
|
||||
apt install apache2-utils
|
||||
```
|
||||
|
||||
Create your user (let's call it `specter`) and type the password:
|
||||
|
||||
```sh
|
||||
htpasswd -c /etc/nginx/.htpasswd specter
|
||||
```
|
||||
|
||||
Add two lines to the server block in the nginx config (`/etc/nginx/sites-enabled/default`):
|
||||
|
||||
```sh
|
||||
server {
|
||||
# ...
|
||||
|
||||
auth_basic "You shall not pass!";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:25441;
|
||||
}
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
Restart nginx:
|
||||
|
||||
```sh
|
||||
nginx -s reload
|
||||
```
|
||||
|
||||
Now when you try to access the server it will ask for credentials.
|
||||
72
docs/self-signed-certificates.md
Normal file
72
docs/self-signed-certificates.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Issuing self-signed certificates
|
||||
|
||||
Browsers require secure communication with the server to use camera API. Without it we can't use QR code scanning.
|
||||
|
||||
If you are running a VPS it's easy - you just [issue a new certificate](./reverse-proxy#adding-https) with Letsencrypt.
|
||||
|
||||
If you are only using the node at home and want to use it from your local network you need to issue a certificate yourself.
|
||||
|
||||
On your node run this command:
|
||||
|
||||
```sh
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
|
||||
```
|
||||
|
||||
It will create two files - `cert.pem` and `key.pem`.
|
||||
|
||||
## Bare Specter over HTTPS
|
||||
|
||||
Provide these files to Specter as arguments:
|
||||
|
||||
```sh
|
||||
python -m cryptoadvance.specter server --cert=./cert.pem --key=./key.pem
|
||||
```
|
||||
|
||||
*Note:* Adding `--tor=your-tor-password` will create a tor hidden service with https.
|
||||
|
||||
## Specter with Nginx
|
||||
|
||||
Assuming you copied the files to `/etc/ssl/certs` and `/etc/ssl/private` add the following lines to server config (`/etc/nginx/sites-enabled/default`):
|
||||
|
||||
```sh
|
||||
listen 443 ssl http2;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
|
||||
```
|
||||
|
||||
My config looks like this:
|
||||
|
||||
```
|
||||
server{
|
||||
listen 80 default_server;
|
||||
listen 443 ssl http2;
|
||||
|
||||
server_name your_domain_or_ip;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:25441;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding certificate to trusted
|
||||
|
||||
With these certificates you should be able to navigate to your node using https, but you will see a scary warning.
|
||||
|
||||
In Firefox you can still proceed to the website, in Chrome you can't unless you add `cert.pem` file to trusted (on the phone you still can).
|
||||
|
||||
On **Mac**: copy `cert.pem` to your computer, add it to your keychain and set `Trust`:
|
||||
|
||||
- Don't forget to quit Chrome
|
||||
- Start the Keychain Access app and open the `Certificates` category
|
||||
- Drag your certificate file onto the Keychain Access window
|
||||
- Right-click on your certificate and unfold the `Trust` list
|
||||
- In row `When using this certificate`, choose `Always Trust`
|
||||
|
||||
Other platforms: ???
|
||||
30
docs/tor.md
30
docs/tor.md
|
|
@ -1,4 +1,5 @@
|
|||
## Running Specter Desktop over a Tor hidden service
|
||||
|
||||
Specter Desktop protects your security and privacy by running on a local server that only talks to your own bitcoin node. But what if you need to check a wallet balance or generate and sign transactions when you're away from your home network?
|
||||
|
||||
Configuring your router to let you VPN into your home network is probably the easiest solution.
|
||||
|
|
@ -14,15 +15,15 @@ Install Tor on the same server that you'll be running Specter Desktop:
|
|||
* [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.
|
||||
```
|
||||
```sh
|
||||
## The port on which Tor will listen for local connections from Tor
|
||||
## controller applications, as documented in control-spec.txt.
|
||||
ControlPort 9051
|
||||
|
|
@ -36,10 +37,18 @@ Restart the Tor service:
|
|||
* `sudo /etc/init.d/tor restart` on linux
|
||||
* `brew services restart tor` on macOS Homebrew installs
|
||||
|
||||
### Running with Tor using command line
|
||||
|
||||
### Configure Specter Desktop to connect to Tor
|
||||
Update the `.flaskenv` file in the project root. Set `CONNECT_TOR` to 'True' and set `FLASK_ENV` to 'production':
|
||||
You can start the server and provide your tor password using `--tor=` flag:
|
||||
|
||||
```sh
|
||||
$ python3 -m cryptoadvance.specter server --tor=your-tor-passphrase
|
||||
```
|
||||
|
||||
### Configure environment variables
|
||||
|
||||
Update the `.flaskenv` file in the project root. Set `CONNECT_TOR` to 'True' and set `FLASK_ENV` to 'production':
|
||||
```sh
|
||||
PORT=25441
|
||||
|
||||
# If you want to serve over a Tor hidden service, also set FLASK_ENV=production.
|
||||
|
|
@ -51,24 +60,27 @@ FLASK_ENV=production
|
|||
```
|
||||
|
||||
### 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:
|
||||
```
|
||||
$ python server.py
|
||||
|
||||
```sh
|
||||
$ python3 -m cryptoadvance.specter server
|
||||
```
|
||||
|
||||
Amongst the startup output you'll see:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ urllib3==1.25.6
|
|||
Werkzeug==0.16.0
|
||||
serial==0.0.97
|
||||
pyserial==3.4
|
||||
daemonize==2.5.0
|
||||
pyOpenSSL==19.1.0
|
||||
# only for testing currently
|
||||
docker==4.1.0
|
||||
pytest==5.2.2
|
||||
|
|
@ -13,6 +13,10 @@ from .bitcoind import (BitcoindDockerController,
|
|||
from .helpers import load_jsons, which
|
||||
from .server import DATA_FOLDER, create_app
|
||||
|
||||
from daemonize import Daemonize
|
||||
from os import path
|
||||
import signal
|
||||
|
||||
DEBUG = True
|
||||
|
||||
@click.group()
|
||||
|
|
@ -21,7 +25,43 @@ def cli():
|
|||
|
||||
|
||||
@cli.command()
|
||||
def server():
|
||||
@click.option("--daemon", is_flag=True)
|
||||
@click.option("--stop", is_flag=True)
|
||||
@click.option("--restart", is_flag=True)
|
||||
@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
|
||||
# for https:
|
||||
@click.option("--cert")
|
||||
@click.option("--key")
|
||||
# provide tor password here
|
||||
@click.option("--tor")
|
||||
def server(daemon, stop, restart, force, port, host, cert, key, tor):
|
||||
# we will store our daemon PIN here
|
||||
pid_file = path.expanduser(path.join(DATA_FOLDER, "daemon.pid"))
|
||||
toraddr_file = path.expanduser(path.join(DATA_FOLDER, "onion.txt"))
|
||||
# check if pid file exists
|
||||
if path.isfile(pid_file):
|
||||
# if we need to stop daemon
|
||||
if stop or restart:
|
||||
print("Stopping the Specter server...")
|
||||
with open(pid_file) as f:
|
||||
pid = int(f.read())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
elif daemon:
|
||||
if not force:
|
||||
print(f"PID file \"{pid_file}\" already exists. Use --force to overwrite")
|
||||
return
|
||||
if stop:
|
||||
return
|
||||
else:
|
||||
if stop or restart:
|
||||
print(f"Can't find PID file \"{pid_file}\"")
|
||||
if stop:
|
||||
return
|
||||
|
||||
app = create_app()
|
||||
# watch templates folder to reload when something changes
|
||||
extra_dirs = ['templates']
|
||||
|
|
@ -33,15 +73,67 @@ def server():
|
|||
if os.path.isfile(filename):
|
||||
extra_files.append(filename)
|
||||
|
||||
# Note: dotenv doesn't convert bools!
|
||||
if os.getenv('CONNECT_TOR', 'False') == 'True' and os.getenv('TOR_PASSWORD') is not None:
|
||||
import tor_util
|
||||
tor_util.run_on_hidden_service(
|
||||
app, port=os.getenv('PORT'),
|
||||
debug=DEBUG, extra_files=extra_files
|
||||
)
|
||||
# if port is not defined - get it from environment
|
||||
if port is None:
|
||||
port = int(os.getenv('PORT', 25441))
|
||||
else:
|
||||
app.run(port=os.getenv('PORT'), debug=DEBUG, extra_files=extra_files)
|
||||
port = int(port)
|
||||
|
||||
# certificates
|
||||
if cert is None:
|
||||
cert = os.getenv('CERT', None)
|
||||
if key is None:
|
||||
key = os.getenv('KEY', None)
|
||||
|
||||
protocol = "http"
|
||||
kwargs = {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"extra_files": extra_files,
|
||||
}
|
||||
if cert is not None and key is not None:
|
||||
cert = os.path.abspath(cert)
|
||||
key = os.path.abspath(key)
|
||||
kwargs["ssl_context"] = (cert, key)
|
||||
protocol = "https"
|
||||
|
||||
# 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')
|
||||
|
||||
def run(debug=False):
|
||||
# Note: dotenv doesn't convert bools!
|
||||
if tor is not None:
|
||||
from . import tor_util
|
||||
# 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.run(debug=debug, **kwargs)
|
||||
|
||||
# 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)
|
||||
# Note: we can't run flask as a deamon in debug mode,
|
||||
# so use debug=False by default
|
||||
d = Daemonize(app="specter", pid=pid_file, action=run)
|
||||
d.start()
|
||||
# if not a daemon we can use DEBUG
|
||||
else:
|
||||
run(DEBUG)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--debug/--no-debug', default=False)
|
||||
|
|
|
|||
|
|
@ -332,6 +332,13 @@ class WalletManager:
|
|||
self.cli = cli
|
||||
if self.working_folder is not None:
|
||||
self._wallets = load_jsons(self.working_folder, key="name")
|
||||
try:
|
||||
existing_wallets = [w["name"] for w in self.cli.listwalletdir()["wallets"]]
|
||||
for k in self._wallets:
|
||||
if self.path+k not in existing_wallets:
|
||||
self._wallets.pop(w["name"])
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self._wallets = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import os
|
||||
import stem
|
||||
from stem.control import Controller
|
||||
from .server import DATA_FOLDER
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv() # Load the secrets from .env
|
||||
def run_on_hidden_service(app, tor_password=None, tor_port=80, save_address_to=None, **kwargs):
|
||||
port = 5000 # default flask port
|
||||
if "port" in kwargs:
|
||||
port = kwargs["port"]
|
||||
else:
|
||||
kwargs["port"] = port
|
||||
|
||||
|
||||
def run_on_hidden_service(app, port, debug, extra_files):
|
||||
with Controller.from_port() as controller:
|
||||
print(' * Connecting to tor')
|
||||
controller.authenticate(os.getenv('TOR_PASSWORD'))
|
||||
controller.authenticate(tor_password)
|
||||
|
||||
key_path = os.path.expanduser('.tor_service_key')
|
||||
key_path = os.path.abspath(os.path.expanduser(os.path.join(DATA_FOLDER,'.tor_service_key')))
|
||||
tor_service_id = None
|
||||
|
||||
if not os.path.exists(key_path):
|
||||
service = controller.create_ephemeral_hidden_service({80: port}, await_publication = True)
|
||||
service = controller.create_ephemeral_hidden_service({tor_port: port}, await_publication = True)
|
||||
tor_service_id = service.service_id
|
||||
print("Started a new hidden service with the address of %s.onion" % tor_service_id)
|
||||
print("* Started a new hidden service with the address of %s.onion" % tor_service_id)
|
||||
|
||||
with open(key_path, 'w') as key_file:
|
||||
key_file.write('%s:%s' % (service.private_key_type, service.private_key))
|
||||
|
|
@ -25,12 +28,17 @@ def run_on_hidden_service(app, port, debug, extra_files):
|
|||
with open(key_path) as key_file:
|
||||
key_type, key_content = key_file.read().split(':', 1)
|
||||
|
||||
service = controller.create_ephemeral_hidden_service({80: port}, key_type = key_type, key_content = key_content, await_publication = True)
|
||||
service = controller.create_ephemeral_hidden_service({tor_port: port}, key_type = key_type, key_content = key_content, await_publication = True)
|
||||
tor_service_id = service.service_id
|
||||
print("Resumed %s.onion" % tor_service_id)
|
||||
print("* Resumed %s.onion" % tor_service_id)
|
||||
|
||||
# save address to file
|
||||
if save_address_to is not None:
|
||||
with open(save_address_to, "w") as f:
|
||||
f.write("%s.onion" % tor_service_id)
|
||||
|
||||
try:
|
||||
app.run(port=port, debug=debug, extra_files=extra_files)
|
||||
app.run(**kwargs)
|
||||
finally:
|
||||
if tor_service_id:
|
||||
print(" * Shutting down our hidden service")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue