specter-desktop/src/cryptoadvance/specter/server.py
k9ert 9df0b77f8c
Feature: Spectrum addition (#1952)
* add spectrum

* rc3 of spectrum

* spectrum rc4

* missing __init__.py

* changes for rc6

* make cli-arguments work in electron

* update spectrum to remove simplejson

* Bugfix devices not shown

* additional checks for wallet_import

* nodes template magic to improve extensibility

* smaller bugfixes

* update spectrum

* keep python 3.9 in pre-commit-yaml

* specter_added_to_flask_app callback added

* useful logger + avoid writing to config with no node alias

* Using FlaskThread

* remove overriding chain property in node.py

* nodes_by_chain method added to node mananger

* icons for spectrum setup

* keep external_node and fix test

* bump spectrum version

* more visibility

* deactivate checker_threads for testing

Co-authored-by: k9ert <kim@swanbitcoin.com>
Co-authored-by: Manolis Mandrapilias <70536101+moneymanolis@users.noreply.github.com>
Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
2022-11-30 13:13:16 +01:00

286 lines
10 KiB
Python

import logging
import os
import sys
from pathlib import Path
from cryptoadvance.specter.hwi_rpc import HWIBridge
from cryptoadvance.specter.liquid.rpc import LiquidRPC
from cryptoadvance.specter.managers.service_manager import ServiceManager
from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.services import callbacks
from cryptoadvance.specter.util.reflection import get_template_static_folder
from dotenv import load_dotenv
from flask import Flask, jsonify, redirect, request, session, url_for
from flask_apscheduler import APScheduler
from .htmlsafebabel import HTMLSafeBabel
from flask_login import LoginManager, login_user
from flask_wtf.csrf import CSRFProtect
from jinja2 import select_autoescape
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.wrappers import Response
from .hwi_server import hwi_server
from .services.callbacks import after_serverpy_init_app, specter_added_to_flask_app
from .specter import Specter
from .util.specter_migrator import SpecterMigrator
logger = logging.getLogger(__name__)
env_path = Path(".") / ".flaskenv"
load_dotenv(env_path)
csrf = CSRFProtect()
class SpecterFlask(Flask):
@property
def supported_languages(self):
return self.config["LANGUAGES"]
def get_language_code(self):
"""
Helper for Babel and other related language selection tasks.
"""
if "language_code" in session:
# Explicit selection
return session["language_code"]
else:
# autodetect
return request.accept_languages.best_match(self.supported_languages.keys())
def set_language_code(self, language_code):
session["language_code"] = language_code
session["is_language_rtl"] = language_code in self.config["RTL_LANGUAGES"]
def calc_module_name(config):
"""tiny helper to make passing configs more convenient"""
if "." in config:
return config
else:
return "cryptoadvance.specter.config." + config
def create_app(config=None):
"""config is either a string:
* if it's with dots, it's fqn classname
* without dots cryptoadvance.specter.config will be added in the front
ir it's a class directly. Then it's simply passed through
"""
# Cmdline has precedence over Env-Var
if config is not None:
if isinstance(config, str):
config = calc_module_name(
os.environ.get("SPECTER_CONFIG")
if os.environ.get("SPECTER_CONFIG")
else config
)
config_name = config
elif isinstance(config, type):
# Useful for testing passing in classes directly
config_name = config.__module__ + "." + config.__name__
else:
# Enables injection of a different config via Env-Variable
if os.environ.get("SPECTER_CONFIG"):
config = calc_module_name(os.environ.get("SPECTER_CONFIG"))
else:
# Default
config = "cryptoadvance.specter.config.ProductionConfig"
config_name = config
app = SpecterFlask(
__name__,
template_folder=get_template_static_folder("templates"),
static_folder=get_template_static_folder("static"),
)
app.tor_service_id = None
app.tor_enabled = False
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
logger.info(f"Configuration: {config}")
app.config.from_object(config)
logger.info(f"SPECTER_DATA_FOLDER: {app.config['SPECTER_DATA_FOLDER']}")
# Might be convenient to know later where it came from (see Service configuration)
app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"] = config_name
if not app.config.get("ENABLE_WERZEUG_REQUEST_LOGGING"):
logging.getLogger("werkzeug").disabled = True
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1
)
csrf.init_app(app)
app.csrf = csrf
return app
def init_app(app: SpecterFlask, hwibridge=False, specter=None):
"""see blogpost 19nd Feb 2020"""
# Configuring a prefix for the app
if app.config["APP_URL_PREFIX"] != "":
# https://dlukes.github.io/flask-wsgi-url-prefix.html
app.wsgi_app = DispatcherMiddleware(
Response("Not Found", status=404),
{app.config["APP_URL_PREFIX"]: app.wsgi_app},
)
# First: Migrations
mm = SpecterMigrator(app.config["SPECTER_DATA_FOLDER"])
mm.execute_migrations()
# Login via Flask-Login
app.logger.info("Initializing LoginManager")
app.secret_key = app.config["SECRET_KEY"]
BitcoinRPC.default_timeout = app.config["BITCOIN_RPC_TIMEOUT"]
LiquidRPC.default_timeout = app.config["LIQUID_RPC_TIMEOUT"]
if specter is None:
# the default. If not None, then it got injected for testing
app.logger.info(
f"Initializing Specter with data-folder {app.config['SPECTER_DATA_FOLDER']}"
)
specter = Specter(
data_folder=app.config["SPECTER_DATA_FOLDER"],
config=app.config["DEFAULT_SPECTER_CONFIG"],
internal_bitcoind_version=app.config["INTERNAL_BITCOIND_VERSION"],
initialize=False,
)
# ServiceManager will instantiate and register blueprints for extensions
# It's an attribute to the specter but specter is not aware of it.
# However some managers are aware of it and so we need to split
# instantiation from initializing and in between attach the service_manager
specter.service_manager = ServiceManager(
specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"]
)
specter.initialize()
# HWI
specter.hwi = HWIBridge()
login_manager = LoginManager()
login_manager.session_protection = app.config.get("SESSION_PROTECTION", "strong")
login_manager.init_app(app) # Enable Login
login_manager.login_view = "auth_endpoint.login" # Enable redirects if unauthorized
app.config["SESSION_COOKIE_SAMESITE"] = "Strict"
@login_manager.user_loader
def user_loader(id):
return specter.user_manager.get_user(id)
def login(id, password: str = None):
user = user_loader(id)
login_user(user)
if password:
# Use the password while we have it to decrypt any protected
# user data (e.g. services).
user.decrypt_user_secret(password)
app.login = login
# Attach specter instance so child views (e.g. hwi) can access it
app.specter = specter
# Executing callback specter_added_to_flask_app
app.logger.info("Executing callback specter_added_to_flask_app ...")
specter.service_manager.execute_ext_callbacks(specter_added_to_flask_app)
if specter.config["auth"].get("method") == "none":
app.logger.info("Login disabled")
app.config["LOGIN_DISABLED"] = True
else:
app.logger.info("Login enabled")
app.config["LOGIN_DISABLED"] = False
app.logger.info("Initializing Controller ...")
app.register_blueprint(hwi_server, url_prefix="/hwi")
csrf.exempt(hwi_server)
if not hwibridge:
with app.app_context():
from cryptoadvance.specter.server_endpoints import controller
from cryptoadvance.specter.services import controller as serviceController
if app.config.get("TESTING"):
logger.info(f"We have {len(app.view_functions)} view Functions")
if app.config.get("TESTING") and len(app.view_functions) <= 51:
# Need to force a reload as otherwise the import is skipped
# in pytest, the app is created anew for each test
# But we shouldn't do that if not necessary as this would result in
# --> View function mapping is overwriting an existing endpoint function
# see archblog for more about this nasty workaround
import importlib
logger.info("Reloading controllers")
importlib.reload(controller)
importlib.reload(serviceController)
else:
@app.route("/", methods=["GET"])
def index():
return redirect(url_for("hwi_server.hwi_bridge_settings"))
if app.config["SPECTER_API_ACTIVE"]:
app.logger.info("Initializing REST ...")
from cryptoadvance.specter.api import api_bp
app.register_blueprint(api_bp)
@app.context_processor
def inject_tor():
if app.config["DEBUG"]:
return dict(tor_service_id="", tor_enabled=False)
return dict(tor_service_id=app.tor_service_id, tor_enabled=app.tor_enabled)
# --------------------- Babel integration ---------------------
if getattr(sys, "frozen", False):
app.config["BABEL_TRANSLATION_DIRECTORIES"] = os.path.join(
sys._MEIPASS, "translations"
)
babel = HTMLSafeBabel(app)
@babel.localeselector
def get_language_code():
# Enables Babel to auto-detect current language
return app.get_language_code()
@app.route("/set_language", methods=["POST"])
def set_language_code():
json_data = request.get_json()
if (
"language_code" in json_data
and json_data["language_code"] in app.supported_languages
):
app.set_language_code(json_data["language_code"])
return jsonify(success=True)
else:
return jsonify(success=False)
# --------------------- Babel integration ---------------------
# Background Scheduler
def every5seconds():
ctx = app.app_context()
ctx.push()
app.specter.service_manager.execute_ext_callbacks(callbacks.every5seconds)
ctx.pop()
# initialize scheduler
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
logger.info("----> starting service callback_after_serverpy_init_app ")
specter.service_manager.execute_ext_callbacks(
after_serverpy_init_app, scheduler=scheduler
)
return app
def create_and_init(config="cryptoadvance.specter.config.ProductionConfig"):
"""This method can be used to fill the FLASK_APP-env variable like
export FLASK_APP="src/cryptoadvance/specter/server:create_and_init()"
See Development.md to use this for debugging
"""
app = create_app(config)
with app.app_context():
init_app(app)
return app