import json from datetime import datetime, timedelta, timezone from typing import Optional from flask import Blueprint, render_template, request, url_for, redirect from flask.typing import ResponseReturnValue from jinja2.utils import markupsafe from sqlalchemy import desc, or_, func 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.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.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(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(storage, url_prefix="/state") 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 markupsafe.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(input: Optional[str]) -> str: if not input: return "None" return json.dumps(json.loads(input), 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)