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 flask import flash
from flask import session
from apscheduler . schedulers . background import BackgroundScheduler
from capsulflask . shared import my_exec_info_message
from capsulflask import hub_model , spoke_model , cli
from capsulflask . payment import try_reconnnect_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 \n to: { ' , ' . join ( message . recipients ) } \n subject: { message . subject } \n body: \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 " ) ,
LIBVIRT_DNSMASQ_PATH = os . environ . get ( " LIBVIRT_DNSMASQ_PATH " , default = " /var/lib/libvirt/dnsmasq " ) . rstrip ( " / " ) ,
# 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 = " " )
)
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 . warn ( " 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 ' ] ) )
if app . config [ ' BTCPAY_URL ' ] != " " :
try_reconnnect_btcpay ( app )
# 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 '
if current_app . config [ ' BROADCAST_BANNER_MESSAGE ' ] is not None and current_app . config [ ' BROADCAST_BANNER_MESSAGE ' ] != " " :
for t in session . get ( " _flashes " , [ ] ) :
if t is not None and t [ 1 ] == current_app . config [ ' BROADCAST_BANNER_MESSAGE ' ] :
return response
flash ( current_app . config [ ' BROADCAST_BANNER_MESSAGE ' ] )
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 )
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 )