from datetime import datetime, timedelta, timezone from typing import Optional from flask import Blueprint, render_template, request from flask.typing import ResponseReturnValue from jinja2.markupsafe import Markup from sqlalchemy import desc, or_ from app.alarms import alarms_for from app.models.activity import Activity from app.models.alarms import Alarm, AlarmState from app.models.bridges import Bridge from app.models.mirrors import Origin, Proxy from app.models.base import Group from app.models.onions import Eotk from app.portal.automation import bp as automation from app.portal.bridgeconf import bp as bridgeconf from app.portal.bridge import bp as bridge 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.origin import bp as origin from app.portal.onion import bp as onion from app.portal.proxy import bp as proxy from app.portal.smart_proxy import bp as smart_proxy 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(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(proxy, url_prefix="/proxy") portal.register_blueprint(smart_proxy, url_prefix="/smart") portal.register_blueprint(webhook, url_prefix="/webhook") @portal.app_template_filter("mirror_expiry") def calculate_mirror_expiry(s: datetime) -> str: expiry = s + timedelta(days=3) countdown = expiry - datetime.utcnow() 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( # type: ignore[no-untyped-call] 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 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") proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed.is_(None)).all() origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).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)