capsul.org webapp
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 315 lines 13 kB view raw
1import logging 2from logging.config import dictConfig as logging_dict_config 3 4import atexit 5import os 6import hashlib 7import requests 8import sys 9import json 10 11import stripe 12from dotenv import load_dotenv, find_dotenv 13from flask import Flask 14from flask_mail import Mail, Message 15from flask import render_template 16from flask import url_for 17from flask import current_app 18from flask import flash 19from flask import session 20from apscheduler.schedulers.background import BackgroundScheduler 21 22 23from capsulflask.shared import my_exec_info_message 24from capsulflask import hub_model, spoke_model, cli 25from capsulflask.payment import try_reconnnect_btcpay 26from capsulflask.http_client import MyHTTPClient 27 28 29load_dotenv(find_dotenv()) 30 31app = Flask(__name__) 32 33app.config.from_mapping( 34 BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"), 35 SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"), 36 HUB_MODE_ENABLED=os.environ.get("HUB_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], 37 SPOKE_MODE_ENABLED=os.environ.get("SPOKE_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], 38 INTERNAL_HTTP_TIMEOUT_SECONDS=os.environ.get("INTERNAL_HTTP_TIMEOUT_SECONDS", default="300"), 39 HUB_MODEL=os.environ.get("HUB_MODEL", default="capsul-flask"), 40 SPOKE_MODEL=os.environ.get("SPOKE_MODEL", default="mock"), 41 LOG_LEVEL=os.environ.get("LOG_LEVEL", default="INFO"), 42 SPOKE_HOST_ID=os.environ.get("SPOKE_HOST_ID", default="baikal"), 43 SPOKE_HOST_TOKEN=os.environ.get("SPOKE_HOST_TOKEN", default="changeme"), 44 HUB_TOKEN=os.environ.get("HUB_TOKEN", default="changeme"), 45 LIBVIRT_DNSMASQ_PATH=os.environ.get("LIBVIRT_DNSMASQ_PATH", default="/var/lib/libvirt/dnsmasq").rstrip("/"), 46 47 48 49 # https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS 50 # https://stackoverflow.com/questions/56332906/where-to-put-ssl-certificates-when-trying-to-connect-to-a-remote-database-using 51 # TLS example: sslmode=verify-full sslrootcert=letsencrypt-root-ca.crt host=db.example.com port=5432 user=postgres password=dev dbname=postgres 52 POSTGRES_CONNECTION_PARAMETERS=os.environ.get( 53 "POSTGRES_CONNECTION_PARAMETERS", 54 default="host=localhost port=5432 user=postgres password=dev dbname=postgres" 55 ), 56 57 DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"), 58 59 MAIL_SERVER=os.environ.get("MAIL_SERVER", default=""), 60 MAIL_PORT=os.environ.get("MAIL_PORT", default="465"), 61 MAIL_USE_TLS=os.environ.get("MAIL_USE_TLS", default="False").lower() in ['true', '1', 't', 'y', 'yes'], 62 MAIL_USE_SSL=os.environ.get("MAIL_USE_SSL", default="True").lower() in ['true', '1', 't', 'y', 'yes'], 63 MAIL_USERNAME=os.environ.get("MAIL_USERNAME", default=""), 64 MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""), 65 MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="no-reply@capsul.org"), 66 ADMIN_NOTIFICATION_EMAIL_ADDRESSES=os.environ.get("ADMIN_NOTIFICATION_EMAIL_ADDRESSES", default="support@capsul.org"), 67 ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=os.environ.get("ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES", default="j3s@c3f.net,forest.n.johnson@gmail.com,capsul@cyberia.club"), 68 BANNED_USERS_EMAIL_ADDRESSES=os.environ.get("BANNED_USERS_EMAIL_ADDRESSES", default=""), 69 BANNED_USERS_IP_ADDRESSES=os.environ.get("BANNED_USERS_IP_ADDRESSES", default=""), 70 71 VM_STORAGE_DIR=os.environ.get("VM_STORAGE_DIR", default="/tank/vm"), 72 BACKUP_STORAGE_MOUNTS=os.environ.get("BACKUP_STORAGE_MOUNTS", default="/mnt/backup1"), 73 BACKUP_DEBUG_LOG=os.environ.get("BACKUP_DEBUG_LOG", default="false"), 74 75 MAIL_DELIVERY_MANAGER_URL=os.environ.get("MAIL_DELIVERY_MANAGER_URL", default=""), 76 MAIL_DELIVERY_MANAGER_SECRET=os.environ.get("MAIL_DELIVERY_MANAGER_SECRET", default=""), 77 78 PROMETHEUS_URL=os.environ.get("PROMETHEUS_URL", default="https://prometheus.cyberia.club"), 79 80 STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"), 81 STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""), 82 STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default=""), 83 #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") 84 85 BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"), 86 BTCPAY_URL=os.environ.get("BTCPAY_URL", default="") 87) 88 89app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL']) 90 91class SetLogLevelToDebugForHeartbeatRelatedMessagesFilter(logging.Filter): 92 def is_often_logged_scheduled_task_message(self, thing): 93 # thing_string = "<error>" 94 is_in_string = False 95 try: 96 thing_string = "%s" % thing 97 is_in_string = 'heartbeat-task' in thing_string \ 98 or 'hub/heartbeat' in thing_string \ 99 or 'spoke/heartbeat' in thing_string 100 except: 101 pass 102 # self.warning("is_often_logged_scheduled_task_message(%s): %s", thing_string, is_in_string ) 103 return is_in_string 104 105 def filter(self, record): 106 if app.config['LOG_LEVEL'] == "DEBUG": 107 return True 108 109 related_to_backup_maintenance = next(filter(lambda x: 'backup-maintenance' in ("%s" % x), record.args), None) is not None 110 is_not_error = 'scheduled at' in record.msg or 'executed successfully' in record.msg 111 if related_to_backup_maintenance and is_not_error: 112 return False 113 114 if self.is_often_logged_scheduled_task_message(record.msg): 115 return False 116 for arg in record.args: 117 if self.is_often_logged_scheduled_task_message(arg): 118 return False 119 120 return True 121 122logging_dict_config({ 123 'version': 1, 124 'formatters': {'default': { 125 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', 126 }}, 127 'filters': { 128 'setLogLevelToDebugForHeartbeatRelatedMessages': { 129 '()': SetLogLevelToDebugForHeartbeatRelatedMessagesFilter, 130 } 131 }, 132 'handlers': {'wsgi': { 133 'class': 'logging.StreamHandler', 134 'stream': 'ext://flask.logging.wsgi_errors_stream', 135 'formatter': 'default', 136 'filters': ['setLogLevelToDebugForHeartbeatRelatedMessages'] 137 }}, 138 'root': { 139 'level': app.config['LOG_LEVEL'], 140 'handlers': ['wsgi'] 141 } 142}) 143 144# app.logger.critical("critical") 145# app.logger.error("error") 146# app.logger.warning("warning") 147# app.logger.info("info") 148# app.logger.debug("debug") 149 150stripe.api_key = app.config['STRIPE_SECRET_KEY'] 151stripe.api_version = app.config['STRIPE_API_VERSION'] 152 153class StdoutMockFlaskMail: 154 def send(self, message: Message): 155 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") 156 157class DeliveryManagerBasedFlaskMail: 158 def send(self, message: Message): 159 responses = [] 160 send_to = message.recipients if message.recipients is not None else [] 161 if message.bcc is not None: 162 send_to = send_to + message.bcc 163 164 for r in send_to: 165 delivery_manager_headers = {'Authorization': f"{app.config['MAIL_DELIVERY_MANAGER_SECRET']}"} 166 delivery_manager_url = f"{app.config['MAIL_DELIVERY_MANAGER_URL']}/send" 167 payload = json.dumps({ 168 'To': r, 169 'Subject': message.subject, 170 'Body': message.body 171 }) 172 response = requests.post(delivery_manager_url, headers=delivery_manager_headers, data=payload) 173 if response.ok: 174 responses.append(response.json()) 175 else: 176 app.logger.error(f"failed to contact delivery_manager_url={delivery_manager_url} : HTTP {response.status_code}: {response.text}") 177 responses.append({ 178 'Attempted': False, 179 'Delivered': False, 180 'Error': response.text, 181 'WaitUntilTimestampMs': 0 182 }) 183 184 return responses 185 186if app.config['MAIL_DELIVERY_MANAGER_URL'] != "": 187 app.config['FLASK_MAIL_INSTANCE'] = DeliveryManagerBasedFlaskMail() 188elif app.config['MAIL_SERVER'] != "": 189 app.config['FLASK_MAIL_INSTANCE'] = Mail(app) 190else: 191 app.logger.warn("No MAIL_SERVER configured. capsul will simply print emails to stdout.") 192 app.config['FLASK_MAIL_INSTANCE'] = StdoutMockFlaskMail() 193 194app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) 195 196if app.config['BTCPAY_URL'] != "": 197 try_reconnnect_btcpay(app) 198 199# only start the scheduler and attempt to migrate the database if we are running the app. 200# otherwise we are running a CLI command. 201command_line = ' '.join(sys.argv) 202is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line) 203 204app.logger.info(f"is_running_server: {is_running_server}") 205 206if app.config['HUB_MODE_ENABLED']: 207 208 if app.config['HUB_MODEL'] == "capsul-flask": 209 app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub(app) 210 211 # debug mode (flask reloader) runs two copies of the app. When running in debug mode, 212 # we only want to start the scheduler one time. 213 if is_running_server and (not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true'): 214 scheduler = BackgroundScheduler() 215 hub_token_headers = {'Authorization': f"Bearer {app.config['HUB_TOKEN']}"} 216 217 heartbeat_task_url = f"{app.config['HUB_URL']}/hub/heartbeat-task" 218 trigger_heartbeat_task = lambda: requests.post(heartbeat_task_url, headers=hub_token_headers) 219 scheduler.add_job(name="heartbeat-task", func=trigger_heartbeat_task, trigger="interval", seconds=5) 220 221 seconds_in_a_day=86400 222 payment_maintenance_task_url = f"{app.config['HUB_URL']}/hub/payment-maintenance" 223 trigger_payment_maintenance_task = lambda: requests.post(payment_maintenance_task_url, headers=hub_token_headers) 224 scheduler.add_job(name="payment-maintenance", func=trigger_payment_maintenance_task, trigger="interval", seconds=seconds_in_a_day) 225 226 backup_maintenance_task_url = f"{app.config['HUB_URL']}/hub/backup-maintenance" 227 trigger_backup_maintenance_task = lambda: requests.post(backup_maintenance_task_url, headers=hub_token_headers) 228 scheduler.add_job(name="backup-maintenance", func=trigger_backup_maintenance_task, trigger="interval", seconds=15) 229 230 scheduler.start() 231 232 atexit.register(lambda: scheduler.shutdown()) 233 234 else: 235 app.config['HUB_MODEL'] = hub_model.MockHub() 236 237 from capsulflask import db 238 db.init_app(app, is_running_server) 239 240 from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin 241 242 app.register_blueprint(landing.bp) 243 app.register_blueprint(auth.bp) 244 app.register_blueprint(console.bp) 245 app.register_blueprint(payment.bp) 246 app.register_blueprint(metrics.bp) 247 app.register_blueprint(cli.bp) 248 app.register_blueprint(hub_api.bp) 249 app.register_blueprint(admin.bp) 250 251 app.add_url_rule("/", endpoint="index") 252 253 254 255if app.config['SPOKE_MODE_ENABLED']: 256 257 if app.config['SPOKE_MODEL'] == "shell-scripts": 258 app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke(app) 259 else: 260 app.config['SPOKE_MODEL'] = spoke_model.MockSpoke() 261 262 from capsulflask import spoke_api 263 264 app.register_blueprint(spoke_api.bp) 265 266@app.after_request 267def security_headers(response): 268 response.headers['X-Frame-Options'] = 'SAMEORIGIN' 269 if 'Content-Security-Policy' not in response.headers: 270 response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'" 271 response.headers['X-Content-Type-Options'] = 'nosniff' 272 273 if current_app.config['BROADCAST_BANNER_MESSAGE'] is not None and current_app.config['BROADCAST_BANNER_MESSAGE'] != "": 274 for t in session.get("_flashes", []): 275 if t is not None and t[1] == current_app.config['BROADCAST_BANNER_MESSAGE']: 276 return response 277 278 flash(current_app.config['BROADCAST_BANNER_MESSAGE']) 279 280 return response 281 282 283@app.context_processor 284def override_url_for(): 285 """ 286 override the url_for function built into flask 287 with our own custom implementation that busts the cache correctly when files change 288 """ 289 return dict(url_for=url_for_with_cache_bust) 290 291 292def url_for_with_cache_bust(endpoint, **values): 293 """ 294 Add a query parameter based on the hash of the file, this acts as a cache bust 295 """ 296 297 if endpoint == 'static': 298 filename = values.get('filename', None) 299 if filename: 300 if 'STATIC_FILE_HASH_CACHE' not in current_app.config: 301 current_app.config['STATIC_FILE_HASH_CACHE'] = dict() 302 303 if filename not in current_app.config['STATIC_FILE_HASH_CACHE']: 304 filepath = os.path.join(current_app.root_path, endpoint, filename) 305 #print(filepath) 306 if os.path.isfile(filepath) and os.access(filepath, os.R_OK): 307 308 with open(filepath, 'rb') as file: 309 hasher = hashlib.md5() 310 hasher.update(file.read()) 311 current_app.config['STATIC_FILE_HASH_CACHE'][filename] = hasher.hexdigest()[-6:] 312 313 values['q'] = current_app.config['STATIC_FILE_HASH_CACHE'][filename] 314 315 return url_for(endpoint, **values)