import json import logging from datetime import datetime, timedelta, timezone from typing import Optional from flask import Blueprint, redirect, render_template, request, url_for from flask.typing import ResponseReturnValue from markupsafe import Markup from sqlalchemy import desc, func, or_ from app.alarms import alarms_for from app.models.activity import Activity from app.models.alarms import Alarm, AlarmState from app.models.base import Group from app.models.bridges import Bridge from app.models.mirrors import Origin, Proxy from app.models.onions import Eotk from app.portal.automation import bp as automation from app.portal.bridge import bp as bridge from app.portal.bridgeconf import bp as bridgeconf from app.portal.cloud import bp as cloud from app.portal.country import bp as country from app.portal.eotk import bp as eotk from app.portal.group import bp as group from app.portal.list import bp as list_ from app.portal.onion import bp as onion from app.portal.origin import bp as origin from app.portal.pool import bp as pool from app.portal.proxy import bp as proxy from app.portal.smart_proxy import bp as smart_proxy from app.portal.static import bp as static from app.portal.storage import bp as storage from app.portal.webhook import bp as webhook portal = Blueprint( "portal", __name__, template_folder="templates", static_folder="static" ) portal.register_blueprint(automation, url_prefix="/automation") portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf") portal.register_blueprint(bridge, url_prefix="/bridge") portal.register_blueprint(cloud, url_prefix="/cloud") portal.register_blueprint(country, url_prefix="/country") portal.register_blueprint(eotk, url_prefix="/eotk") portal.register_blueprint(group, url_prefix="/group") portal.register_blueprint(list_, url_prefix="/list") portal.register_blueprint(origin, url_prefix="/origin") portal.register_blueprint(onion, url_prefix="/onion") portal.register_blueprint(pool, url_prefix="/pool") portal.register_blueprint(proxy, url_prefix="/proxy") portal.register_blueprint(smart_proxy, url_prefix="/smart") portal.register_blueprint(static, url_prefix="/static") portal.register_blueprint(storage, url_prefix="/state") portal.register_blueprint(webhook, url_prefix="/webhook") @portal.app_template_filter("bridge_expiry") def calculate_bridge_expiry(b: Bridge) -> str: if b.deprecated is None: logging.warning( "Bridge expiry requested by template for a bridge %s that was not expiring.", b.id, ) return "Not expiring" expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours) countdown = expiry - datetime.now(tz=timezone.utc) if countdown.days == 0: return f"{countdown.seconds // 3600} hours" return f"{countdown.days} days" @portal.app_template_filter("mirror_expiry") def calculate_mirror_expiry(s: datetime) -> str: expiry = s + timedelta(days=3) countdown = expiry - datetime.now(tz=timezone.utc) if countdown.days == 0: return f"{countdown.seconds // 3600} hours" return f"{countdown.days} days" @portal.app_template_filter("format_datetime") def format_datetime(s: Optional[datetime]) -> str: if s is None: return "Unknown" return s.strftime("%a, %d %b %Y %H:%M:%S") @portal.app_template_filter("describe_brn") def describe_brn(s: str) -> ResponseReturnValue: parts = s.split(":") if parts[3] == "mirror": if parts[5].startswith("origin/"): origin = Origin.query.filter( Origin.domain_name == parts[5][len("origin/") :] ).first() if not origin: return s return f"Origin: {origin.domain_name} ({origin.group.group_name})" if parts[5].startswith("proxy/"): proxy = Proxy.query.filter( Proxy.id == int(parts[5][len("proxy/") :]) ).first() if not proxy: return s return Markup( f"Proxy: {proxy.url}
({proxy.origin.group.group_name}: {proxy.origin.domain_name})" ) if parts[5].startswith("quota/"): if parts[4] == "cloudfront": return f"Quota: CloudFront {parts[5][len('quota/'):]}" if parts[3] == "eotk": if parts[5].startswith("instance/"): eotk = Eotk.query.filter( Eotk.group_id == parts[2], Eotk.region == parts[5][len("instance/") :] ).first() if not eotk: return s return f"EOTK Instance: {eotk.group.group_name} in {eotk.provider} {eotk.region}" return s @portal.app_template_filter("pretty_json") def pretty_json(json_str: Optional[str]) -> str: if not json_str: return "None" return json.dumps(json.loads(json_str), indent=2) def total_origins_blocked() -> int: count = 0 for o in Origin.query.filter(Origin.destroyed.is_(None)).all(): for a in alarms_for(o.brn): if a.aspect.startswith("origin-block-ooni-"): if a.alarm_state == AlarmState.WARNING: count += 1 break return count @portal.route("/") def portal_home() -> ResponseReturnValue: groups = Group.query.order_by(Group.group_name).all() now = datetime.now(timezone.utc) proxies = Proxy.query.filter(Proxy.destroyed.is_(None)).all() last24 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=1))).all()) last72 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=3))).all()) lastweek = len( Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=7))).all() ) alarms = { s: len( Alarm.query.filter( Alarm.alarm_state == s.upper(), Alarm.last_updated > (now - timedelta(days=1)), ).all() ) for s in ["critical", "warning", "ok", "unknown"] } bridges = Bridge.query.filter(Bridge.destroyed.is_(None)).all() br_last = { d: len(Bridge.query.filter(Bridge.deprecated > (now - timedelta(days=d))).all()) for d in [1, 3, 7] } activity = ( Activity.query.filter(Activity.added > (now - timedelta(days=2))) .order_by(desc(Activity.added)) .all() ) onionified = len( [ o for o in Origin.query.filter(Origin.destroyed.is_(None)).all() if o.onion() is not None ] ) ooni_blocked = total_origins_blocked() total_origins = len(Origin.query.filter(Origin.destroyed.is_(None)).all()) return render_template( "home.html.j2", section="home", groups=groups, last24=last24, last72=last72, lastweek=lastweek, proxies=proxies, **alarms, activity=activity, total_origins=total_origins, onionified=onionified, br_last=br_last, ooni_blocked=ooni_blocked, bridges=bridges, ) @portal.route("/search") def search() -> ResponseReturnValue: query = request.args.get("query") if query is None: return redirect(url_for("portal.portal_home")) proxies = Proxy.query.filter( or_(func.lower(Proxy.url).contains(query.lower())), Proxy.destroyed.is_(None) ).all() origins = Origin.query.filter( or_( func.lower(Origin.description).contains(query.lower()), func.lower(Origin.domain_name).contains(query.lower()), ) ).all() return render_template( "search.html.j2", section="home", proxies=proxies, origins=origins ) @portal.route("/alarms") def view_alarms() -> ResponseReturnValue: one_day_ago = datetime.now(timezone.utc) - timedelta(days=1) alarms = ( Alarm.query.filter(Alarm.last_updated >= one_day_ago) .order_by(desc(Alarm.alarm_state), desc(Alarm.state_changed)) .all() ) return render_template( "list.html.j2", section="alarm", title="Alarms", items=alarms )