forked from 3wordchant/capsul-flask
357d99cb91
..to allow accessing config variables in the templates. This removes the need for adding config variables manually to template contexts.
249 lines
9.5 KiB
Python
249 lines
9.5 KiB
Python
import logging
|
|
from logging.config import dictConfig as logging_dict_config
|
|
|
|
import atexit
|
|
import os
|
|
import hashlib
|
|
import requests
|
|
import sys
|
|
|
|
import stripe
|
|
from dotenv import load_dotenv, find_dotenv
|
|
from flask import Flask
|
|
from flask_mail import Mail, Message
|
|
from flask import render_template
|
|
from flask import url_for
|
|
from flask import current_app
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
|
|
from capsulflask.shared import my_exec_info_message
|
|
from capsulflask import hub_model, spoke_model, cli
|
|
from capsulflask.btcpay import client as btcpay
|
|
from capsulflask.http_client import MyHTTPClient
|
|
|
|
class StdoutMockFlaskMail:
|
|
def send(self, message: Message):
|
|
current_app.logger.info(f"Email would have been sent if configured:\n\nto: {','.join(message.recipients)}\nsubject: {message.subject}\nbody:\n\n{message.body}\n\n")
|
|
|
|
load_dotenv(find_dotenv())
|
|
|
|
app = Flask(__name__)
|
|
|
|
app.config.from_mapping(
|
|
BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"),
|
|
SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"),
|
|
HUB_MODE_ENABLED=os.environ.get("HUB_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
|
|
SPOKE_MODE_ENABLED=os.environ.get("SPOKE_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
|
|
INTERNAL_HTTP_TIMEOUT_SECONDS=os.environ.get("INTERNAL_HTTP_TIMEOUT_SECONDS", default="300"),
|
|
HUB_MODEL=os.environ.get("HUB_MODEL", default="capsul-flask"),
|
|
SPOKE_MODEL=os.environ.get("SPOKE_MODEL", default="mock"),
|
|
LOG_LEVEL=os.environ.get("LOG_LEVEL", default="INFO"),
|
|
SPOKE_HOST_ID=os.environ.get("SPOKE_HOST_ID", default="baikal"),
|
|
SPOKE_HOST_TOKEN=os.environ.get("SPOKE_HOST_TOKEN", default="changeme"),
|
|
HUB_TOKEN=os.environ.get("HUB_TOKEN", default="changeme"),
|
|
|
|
# https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS
|
|
# https://stackoverflow.com/questions/56332906/where-to-put-ssl-certificates-when-trying-to-connect-to-a-remote-database-using
|
|
# TLS example: sslmode=verify-full sslrootcert=letsencrypt-root-ca.crt host=db.example.com port=5432 user=postgres password=dev dbname=postgres
|
|
POSTGRES_CONNECTION_PARAMETERS=os.environ.get(
|
|
"POSTGRES_CONNECTION_PARAMETERS",
|
|
default="host=localhost port=5432 user=postgres password=dev dbname=postgres"
|
|
),
|
|
|
|
DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"),
|
|
|
|
MAIL_SERVER=os.environ.get("MAIL_SERVER", default=""),
|
|
MAIL_PORT=os.environ.get("MAIL_PORT", default="465"),
|
|
MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="False").lower() in ['true', '1', 't', 'y', 'yes'],
|
|
MAIL_USE_SSL=os.environ.get("MAIL_USE_SSL", default="True").lower() in ['true', '1', 't', 'y', 'yes'],
|
|
MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default=""),
|
|
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
|
|
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="no-reply@capsul.org"),
|
|
ADMIN_EMAIL_ADDRESSES=os.environ.get("ADMIN_EMAIL_ADDRESSES", default="ops@cyberia.club"),
|
|
ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=os.environ.get("ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES", default="forest.n.johnson@gmail.com,capsul@cyberia.club"),
|
|
|
|
PROMETHEUS_URL=os.environ.get("PROMETHEUS_URL", default="https://prometheus.cyberia.club"),
|
|
|
|
STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"),
|
|
STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""),
|
|
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default=""),
|
|
#STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="")
|
|
|
|
BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"),
|
|
BTCPAY_URL=os.environ.get("BTCPAY_URL", default="https://btcpay.cyberia.club")
|
|
)
|
|
|
|
app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL'])
|
|
|
|
class SetLogLevelToDebugForHeartbeatRelatedMessagesFilter(logging.Filter):
|
|
def isHeartbeatRelatedString(self, thing):
|
|
# thing_string = "<error>"
|
|
is_in_string = False
|
|
try:
|
|
thing_string = "%s" % thing
|
|
is_in_string = 'heartbeat-task' in thing_string or 'hub/heartbeat' in thing_string or 'spoke/heartbeat' in thing_string
|
|
except:
|
|
pass
|
|
# self.warning("isHeartbeatRelatedString(%s): %s", thing_string, is_in_string )
|
|
return is_in_string
|
|
|
|
def filter(self, record):
|
|
if app.config['LOG_LEVEL'] == "DEBUG":
|
|
return True
|
|
|
|
if self.isHeartbeatRelatedString(record.msg):
|
|
return False
|
|
for arg in record.args:
|
|
if self.isHeartbeatRelatedString(arg):
|
|
return False
|
|
|
|
return True
|
|
|
|
logging_dict_config({
|
|
'version': 1,
|
|
'formatters': {'default': {
|
|
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
|
}},
|
|
'filters': {
|
|
'setLogLevelToDebugForHeartbeatRelatedMessages': {
|
|
'()': SetLogLevelToDebugForHeartbeatRelatedMessagesFilter,
|
|
}
|
|
},
|
|
'handlers': {'wsgi': {
|
|
'class': 'logging.StreamHandler',
|
|
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
|
'formatter': 'default',
|
|
'filters': ['setLogLevelToDebugForHeartbeatRelatedMessages']
|
|
}},
|
|
'root': {
|
|
'level': app.config['LOG_LEVEL'],
|
|
'handlers': ['wsgi']
|
|
}
|
|
})
|
|
|
|
# app.logger.critical("critical")
|
|
# app.logger.error("error")
|
|
# app.logger.warning("warning")
|
|
# app.logger.info("info")
|
|
# app.logger.debug("debug")
|
|
|
|
stripe.api_key = app.config['STRIPE_SECRET_KEY']
|
|
stripe.api_version = app.config['STRIPE_API_VERSION']
|
|
|
|
if app.config['MAIL_SERVER'] != "":
|
|
app.config['FLASK_MAIL_INSTANCE'] = Mail(app)
|
|
else:
|
|
app.logger.warning("No MAIL_SERVER configured. capsul will simply print emails to stdout.")
|
|
app.config['FLASK_MAIL_INSTANCE'] = StdoutMockFlaskMail()
|
|
|
|
app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS']))
|
|
|
|
try:
|
|
app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY'])
|
|
except:
|
|
app.logger.warning("unable to create btcpay client. Capsul will work fine except cryptocurrency payments will not work. The error was: " + my_exec_info_message(sys.exc_info()))
|
|
|
|
# only start the scheduler and attempt to migrate the database if we are running the app.
|
|
# otherwise we are running a CLI command.
|
|
command_line = ' '.join(sys.argv)
|
|
is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line)
|
|
|
|
app.logger.info(f"is_running_server: {is_running_server}")
|
|
|
|
if app.config['HUB_MODE_ENABLED']:
|
|
|
|
if app.config['HUB_MODEL'] == "capsul-flask":
|
|
app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub()
|
|
|
|
# debug mode (flask reloader) runs two copies of the app. When running in debug mode,
|
|
# we only want to start the scheduler one time.
|
|
if is_running_server and (not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true'):
|
|
scheduler = BackgroundScheduler()
|
|
heartbeat_task_url = f"{app.config['HUB_URL']}/hub/heartbeat-task"
|
|
heartbeat_task_headers = {'Authorization': f"Bearer {app.config['HUB_TOKEN']}"}
|
|
heartbeat_task = lambda: requests.post(heartbeat_task_url, headers=heartbeat_task_headers)
|
|
scheduler.add_job(name="heartbeat-task", func=heartbeat_task, trigger="interval", seconds=5)
|
|
scheduler.start()
|
|
|
|
atexit.register(lambda: scheduler.shutdown())
|
|
|
|
else:
|
|
app.config['HUB_MODEL'] = hub_model.MockHub()
|
|
|
|
from capsulflask import db
|
|
db.init_app(app, is_running_server)
|
|
|
|
from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin
|
|
|
|
app.register_blueprint(landing.bp)
|
|
app.register_blueprint(auth.bp)
|
|
app.register_blueprint(console.bp)
|
|
app.register_blueprint(payment.bp)
|
|
app.register_blueprint(metrics.bp)
|
|
app.register_blueprint(cli.bp)
|
|
app.register_blueprint(hub_api.bp)
|
|
app.register_blueprint(admin.bp)
|
|
|
|
app.add_url_rule("/", endpoint="index")
|
|
|
|
|
|
|
|
if app.config['SPOKE_MODE_ENABLED']:
|
|
|
|
if app.config['SPOKE_MODEL'] == "shell-scripts":
|
|
app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke()
|
|
else:
|
|
app.config['SPOKE_MODEL'] = spoke_model.MockSpoke()
|
|
|
|
from capsulflask import spoke_api
|
|
|
|
app.register_blueprint(spoke_api.bp)
|
|
|
|
@app.after_request
|
|
def security_headers(response):
|
|
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
|
if 'Content-Security-Policy' not in response.headers:
|
|
response.headers['Content-Security-Policy'] = "default-src 'self'"
|
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
return response
|
|
|
|
|
|
@app.context_processor
|
|
def override_url_for():
|
|
"""
|
|
override the url_for function built into flask
|
|
with our own custom implementation that busts the cache correctly when files change
|
|
"""
|
|
return dict(url_for=url_for_with_cache_bust)
|
|
|
|
|
|
@app.context_processor
|
|
def load_config_vars():
|
|
return dict(config=app.config)
|
|
|
|
def url_for_with_cache_bust(endpoint, **values):
|
|
"""
|
|
Add a query parameter based on the hash of the file, this acts as a cache bust
|
|
"""
|
|
|
|
if endpoint == 'static':
|
|
filename = values.get('filename', None)
|
|
if filename:
|
|
if 'STATIC_FILE_HASH_CACHE' not in current_app.config:
|
|
current_app.config['STATIC_FILE_HASH_CACHE'] = dict()
|
|
|
|
if filename not in current_app.config['STATIC_FILE_HASH_CACHE']:
|
|
filepath = os.path.join(current_app.root_path, endpoint, filename)
|
|
#print(filepath)
|
|
if os.path.isfile(filepath) and os.access(filepath, os.R_OK):
|
|
|
|
with open(filepath, 'rb') as file:
|
|
hasher = hashlib.md5()
|
|
hasher.update(file.read())
|
|
current_app.config['STATIC_FILE_HASH_CACHE'][filename] = hasher.hexdigest()[-6:]
|
|
|
|
values['q'] = current_app.config['STATIC_FILE_HASH_CACHE'][filename]
|
|
|
|
return url_for(endpoint, **values)
|