From d54fae7423e24a1cf4c4db23e27072a26a24c035 Mon Sep 17 00:00:00 2001 From: Iain Learmonth Date: Thu, 12 May 2022 17:03:26 +0100 Subject: [PATCH] portal: additional buttons on list pages link to onion services page from origin page link to previews of distribution lists from lists --- app/lists/mirror_mapping.py | 2 +- app/models/base.py | 14 ++++ app/portal/__init__.py | 4 +- app/portal/list.py | 53 ++++++++---- app/portal/origin.py | 9 +- app/portal/static/portal.css | 126 +++++++++++++++++----------- app/portal/templates/base.html.j2 | 6 +- app/portal/templates/list.html.j2 | 5 +- app/portal/templates/tables.html.j2 | 6 +- 9 files changed, 151 insertions(+), 74 deletions(-) diff --git a/app/lists/mirror_mapping.py b/app/lists/mirror_mapping.py index 650bbc6..937cdd7 100644 --- a/app/lists/mirror_mapping.py +++ b/app/lists/mirror_mapping.py @@ -4,7 +4,6 @@ from typing import Dict, List from pydantic import BaseModel, Field from tldextract import extract -from app import app from app.models.base import Group from app.models.mirrors import Proxy @@ -31,6 +30,7 @@ class MirrorMapping(BaseModel): def mirror_mapping(): + from app import app return MirrorMapping( version="1.1", mappings={ diff --git a/app/models/base.py b/app/models/base.py index ef435b1..f35cdc1 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -24,11 +24,25 @@ class Group(AbstractConfiguration): class MirrorList(AbstractConfiguration): provider = db.Column(db.String(255), nullable=False) format = db.Column(db.String(20), nullable=False) + # obfuscate = db.Column(db.Boolean(), nullable=False) container = db.Column(db.String(255), nullable=False) branch = db.Column(db.String(255), nullable=False) role = db.Column(db.String(255), nullable=True) filename = db.Column(db.String(255), nullable=False) + providers_supported = { + "github": "GitHub", + "gitlab": "GitLab", + "s3": "AWS S3", + } + + formats_supported = { + "bc2": "Bypass Censorship v2", + "bc3": "Bypass Censorship v3", + "bca": "Bypass Censorship Analytics", + "bridgelines": "Tor Bridge Lines" + } + def destroy(self): self.destroyed = datetime.utcnow() self.updated = datetime.utcnow() diff --git a/app/portal/__init__.py b/app/portal/__init__.py index 18bc1d7..527589f 100644 --- a/app/portal/__init__.py +++ b/app/portal/__init__.py @@ -4,7 +4,7 @@ from flask import Blueprint, render_template, request from sqlalchemy import desc, or_ from app.models.alarms import Alarm -from app import Origin, Proxy +from app.models.mirrors import Origin, Proxy from app.models.base import Group from app.portal.list import NewMirrorListForm from app.portal.automation import bp as automation @@ -76,5 +76,3 @@ def view_alarms(): section="alarm", title="Alarms", items=alarms) - - diff --git a/app/portal/list.py b/app/portal/list.py index cf6477c..d9fe01b 100644 --- a/app/portal/list.py +++ b/app/portal/list.py @@ -1,18 +1,32 @@ +import json from datetime import datetime -from flask import render_template, url_for, flash, redirect, Blueprint +from flask import render_template, url_for, flash, redirect, Blueprint, Response from flask_wtf import FlaskForm from sqlalchemy import exc from wtforms import SelectField, StringField, SubmitField from wtforms.validators import DataRequired -from app import db +from app.extensions import db +from app.lists.bc2 import mirror_sites +from app.lists.bridgelines import bridgelines +from app.lists.mirror_mapping import mirror_mapping from app.models.base import MirrorList from app.portal.util import response_404, view_lifecycle bp = Blueprint("list", __name__) +@bp.app_template_filter("provider_name") +def list_provider_name(s: str) -> str: + return MirrorList.providers_supported.get(s, "Unknown") + + +@bp.app_template_filter("format_name") +def list_format_name(s: str) -> str: + return MirrorList.formats_supported.get(s, "Unknown") + + @bp.route('/list') def list_list(): lists = MirrorList.query.filter(MirrorList.destroyed == None).all() @@ -21,7 +35,27 @@ def list_list(): title="Mirror Lists", item="mirror list", new_link=url_for("portal.list.list_new"), - items=lists) + items=lists, + extra_buttons=[ + { + "link": url_for("portal.list.list_preview", format_=k), + "text": f"Preview {v}", + "style": "secondary" + } + for k, v in MirrorList.formats_supported.items() + ] + ) + + +@bp.route('/preview/') +def list_preview(format_: str): + if format_ == "bca": + return Response(json.dumps(mirror_mapping()), content_type="application/json") + if format_ == "bc2": + return Response(json.dumps(mirror_sites()), content_type="application/json") + if format_ == "bridgelines": + return Response(json.dumps(bridgelines()), content_type="application/json") + return response_404(message="Format not found") @bp.route("/destroy/", methods=['GET', 'POST']) @@ -44,17 +78,8 @@ def list_destroy(list_id: int): @bp.route("/new/", methods=['GET', 'POST']) def list_new(group_id=None): form = NewMirrorListForm() - form.provider.choices = [ - ("github", "GitHub"), - ("gitlab", "GitLab"), - ("s3", "AWS S3"), - ] - form.format.choices = [ - ("bc2", "Bypass Censorship v2"), - ("bc3", "Bypass Censorship v3"), - ("bca", "Bypass Censorship Analytics"), - ("bridgelines", "Tor Bridge Lines") - ] + form.provider.choices = [(k, v) for k, v in MirrorList.providers_supported] + form.format.choices = [(k, v) for k, v in MirrorList.formats_supported] if form.validate_on_submit(): list_ = MirrorList() list_.provider = form.provider.data diff --git a/app/portal/origin.py b/app/portal/origin.py index 81e6b44..b57601e 100644 --- a/app/portal/origin.py +++ b/app/portal/origin.py @@ -89,10 +89,15 @@ def origin_list(): origins = Origin.query.order_by(Origin.domain_name).all() return render_template("list.html.j2", section="origin", - title="Origins", + title="Web Origins", item="origin", new_link=url_for("portal.origin.origin_new"), - items=origins) + items=origins, + extra_buttons=[{ + "link": url_for("portal.origin.origin_onion"), + "text": "Onion services", + "style": "onion" + }]) @bp.route("/onion") diff --git a/app/portal/static/portal.css b/app/portal/static/portal.css index e1099fb..26bf8ce 100644 --- a/app/portal/static/portal.css +++ b/app/portal/static/portal.css @@ -1,11 +1,11 @@ body { - font-size: .875rem; + font-size: .875rem; } .feather { - width: 16px; - height: 16px; - vertical-align: text-bottom; + width: 16px; + height: 16px; + vertical-align: text-bottom; } /* @@ -13,56 +13,56 @@ body { */ .sidebar { - position: fixed; - top: 0; - /* rtl:raw: - right: 0; - */ - bottom: 0; - /* rtl:remove */ - left: 0; - z-index: 100; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); + position: fixed; + top: 0; + /* rtl:raw: + right: 0; + */ + bottom: 0; + /* rtl:remove */ + left: 0; + z-index: 100; /* Behind the navbar */ + padding: 48px 0 0; /* Height of navbar */ + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); } @media (max-width: 767.98px) { - .sidebar { - top: 5rem; - } + .sidebar { + top: 5rem; + } } .sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 48px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ + position: relative; + top: 0; + height: calc(100vh - 48px); + padding-top: .5rem; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ } .sidebar .nav-link { - font-weight: 500; - color: #333; + font-weight: 500; + color: #333; } .sidebar .nav-link .feather { - margin-right: 4px; - color: #727272; + margin-right: 4px; + color: #727272; } .sidebar .nav-link.active { - color: #2470dc; + color: #2470dc; } .sidebar .nav-link:hover .feather, .sidebar .nav-link.active .feather { - color: inherit; + color: inherit; } .sidebar-heading { - font-size: .75rem; - text-transform: uppercase; + font-size: .75rem; + text-transform: uppercase; } /* @@ -70,31 +70,63 @@ body { */ .navbar-brand { - padding-top: .75rem; - padding-bottom: .75rem; - font-size: 1rem; - background-color: rgba(0, 0, 0, .25); - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); + padding-top: .75rem; + padding-bottom: .75rem; + font-size: 1rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); } .navbar .navbar-toggler { - top: .25rem; - right: 1rem; + top: .25rem; + right: 1rem; } .navbar .form-control { - padding: .75rem 1rem; - border-width: 0; - border-radius: 0; + padding: .75rem 1rem; + border-width: 0; + border-radius: 0; } .form-control-dark { - color: #fff; - background-color: rgba(255, 255, 255, .1); - border-color: rgba(255, 255, 255, .1); + color: #fff; + background-color: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .1); } .form-control-dark:focus { - border-color: transparent; - box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); + border-color: transparent; + box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); +} + +.btn-onion { + background-color: #7d4698; + border-color: #7d4698; + color: #fff +} + +.btn-check:focus + .btn-onion, .btn-onion:focus, .btn-onion:hover { + background-color: #6a3c81; + border-color: #64387a; + color: #fff +} + +.btn-check:focus + .btn-onion, .btn-onion:focus { + box-shadow: 0 0 0 .25rem rgba(145, 98, 167, .5) +} + +.btn-check:active + .btn-onion, .btn-check:checked + .btn-onion, .btn-onion.active, .btn-onion:active, .show > .btn-onion.dropdown-toggle { + background-color: #64387a; + border-color: #5e3572; + color: #fff +} + +.btn-check:active + .btn-onion:focus, .btn-check:checked + .btn-onion:focus, .btn-onion.active:focus, .btn-onion:active:focus, .show > .btn-onion.dropdown-toggle:focus { + box-shadow: 0 0 0 .25rem rgba(145, 98, 167, .5) +} + +.btn-onion.disabled, .btn-onion:disabled { + background-color: #7d4698; + border-color: #7d4698; + color: #fff } diff --git a/app/portal/templates/base.html.j2 b/app/portal/templates/base.html.j2 index 7bbfc5b..d38622b 100644 --- a/app/portal/templates/base.html.j2 +++ b/app/portal/templates/base.html.j2 @@ -102,7 +102,7 @@