173 lines
7.2 KiB
Python
173 lines
7.2 KiB
Python
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.cloud import bp as cloud
|
|
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(cloud, url_prefix="/cloud")
|
|
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("bridge_expiry")
|
|
def calculate_bridge_expiry(b: Bridge) -> str:
|
|
expiry = b.deprecated + timedelta(hours=b.conf.expiry_hours)
|
|
countdown = expiry - datetime.utcnow()
|
|
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.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}<br>({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)
|