majuna/app/portal/bridgeconf.py

166 lines
8.3 KiB
Python
Raw Normal View History

from datetime import datetime
2022-05-16 11:44:03 +01:00
from typing import Optional, List
from flask import render_template, url_for, flash, redirect, Response, Blueprint
2022-05-16 11:44:03 +01:00
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
from wtforms import SelectField, StringField, IntegerField, SubmitField
from wtforms.validators import DataRequired, NumberRange
from app.extensions import db
from app.models.base import Pool
from app.models.bridges import BridgeConf, ProviderAllocation
from app.portal.util import response_404, view_lifecycle
bp = Blueprint("bridgeconf", __name__)
_SECTION_TEMPLATE_VARS = {
"section": "bridgeconf",
"help_url": "https://bypass.censorship.guide/user/bridges.html"
}
2022-05-16 11:44:03 +01:00
class NewBridgeConfForm(FlaskForm): # type: ignore
method = SelectField('Distribution Method', validators=[DataRequired()])
description = StringField('Description')
pool = SelectField('Pool', validators=[DataRequired()])
target_number = IntegerField('Target Number',
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")])
max_number = IntegerField('Maximum Number',
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(1, message="Must be at least 1, ideally greater than target number.")])
expiry_hours = IntegerField('Expiry Timer (hours)',
description=("The number of hours to wait after a bridge is deprecated before its "
"destruction."))
provider_allocation = SelectField('Provider Allocation Method',
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
])
submit = SubmitField('Save Changes')
2022-05-16 11:44:03 +01:00
class EditBridgeConfForm(FlaskForm): # type: ignore
description = StringField('Description')
target_number = IntegerField('Target Number',
description="The number of active bridges to deploy (excluding deprecated bridges).",
validators=[NumberRange(1, message="One or more bridges must be created.")])
max_number = IntegerField('Maximum Number',
description="The maximum number of bridges to deploy (including deprecated bridges).",
validators=[
NumberRange(1, message="Must be at least 1, ideally greater than target number.")])
expiry_hours = IntegerField('Expiry Timer (hours)',
description=("The number of hours to wait after a bridge is deprecated before its "
"destruction."))
provider_allocation = SelectField('Provider Allocation Method',
description="How to allocate new bridges to providers.",
choices=[
("COST", "Use cheapest provider first"),
("RANDOM", "Use providers randomly"),
])
submit = SubmitField('Save Changes')
@bp.route("/list")
2022-05-16 11:44:03 +01:00
def bridgeconf_list() -> ResponseReturnValue:
2022-05-16 13:29:48 +01:00
bridgeconfs: List[BridgeConf] = BridgeConf.query.filter(BridgeConf.destroyed.is_(None)).all()
return render_template("list.html.j2",
title="Tor Bridge Configurations",
item="bridge configuration",
items=bridgeconfs,
new_link=url_for("portal.bridgeconf.bridgeconf_new"),
**_SECTION_TEMPLATE_VARS)
@bp.route("/new", methods=['GET', 'POST'])
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
2022-05-16 11:44:03 +01:00
def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = NewBridgeConfForm()
form.pool.choices = [(x.id, x.pool_name) for x in Pool.query.filter(Pool.destroyed.is_(None)).all()]
form.method.choices = [
("any", "Any (BridgeDB)"),
("email", "E-Mail (BridgeDB)"),
("moat", "Moat (BridgeDB)"),
("settings", "Settings (BridgeDB)"),
("https", "HTTPS (BridgeDB)"),
("none", "None (Private)")
]
if form.validate_on_submit():
bridgeconf = BridgeConf()
bridgeconf.pool_id = form.pool.data
bridgeconf.method = form.method.data
bridgeconf.description = form.description.data
bridgeconf.target_number = form.target_number.data
bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.created = datetime.utcnow()
bridgeconf.updated = datetime.utcnow()
try:
db.session.add(bridgeconf)
db.session.commit()
flash(f"Created new bridge configuration {bridgeconf.id}.", "success")
return redirect(url_for("portal.bridgeconf.bridgeconf_list"))
except exc.SQLAlchemyError:
flash("Failed to create new bridge configuration.", "danger")
return redirect(url_for("portal.bridgeconf.bridgeconf_list"))
if group_id:
form.group.data = group_id
return render_template("new.html.j2",
form=form,
**_SECTION_TEMPLATE_VARS)
@bp.route('/edit/<bridgeconf_id>', methods=['GET', 'POST'])
2022-05-16 11:44:03 +01:00
def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue:
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id).first()
if bridgeconf is None:
return Response(render_template("error.html.j2",
header="404 Bridge Configuration Not Found",
message="The requested bridge configuration could not be found.",
**_SECTION_TEMPLATE_VARS),
status=404)
form = EditBridgeConfForm(description=bridgeconf.description,
target_number=bridgeconf.target_number,
max_number=bridgeconf.max_number,
expiry_hours=bridgeconf.expiry_hours,
provider_allocation=bridgeconf.provider_allocation.name,
)
if form.validate_on_submit():
bridgeconf.description = form.description.data
bridgeconf.target_number = form.target_number.data
bridgeconf.max_number = form.max_number.data
bridgeconf.expiry_hours = form.expiry_hours.data
bridgeconf.provider_allocation = ProviderAllocation[form.provider_allocation.data]
bridgeconf.updated = datetime.utcnow()
try:
db.session.commit()
flash("Saved changes to bridge configuration.", "success")
except exc.SQLAlchemyError:
flash("An error occurred saving the changes to the bridge configuration.", "danger")
return render_template("bridgeconf.html.j2",
bridgeconf=bridgeconf,
form=form,
**_SECTION_TEMPLATE_VARS)
@bp.route("/destroy/<bridgeconf_id>", methods=['GET', 'POST'])
2022-05-16 11:44:03 +01:00
def bridgeconf_destroy(bridgeconf_id: int) -> ResponseReturnValue:
2022-05-16 13:29:48 +01:00
bridgeconf = BridgeConf.query.filter(BridgeConf.id == bridgeconf_id, BridgeConf.destroyed.is_(None)).first()
if bridgeconf is None:
return response_404("The requested bridge configuration could not be found.")
return view_lifecycle(
2022-05-16 13:29:48 +01:00
header="Destroy bridge configuration?",
message=bridgeconf.description,
success_view="portal.bridgeconf.bridgeconf_list",
success_message="All bridges from the destroyed configuration will shortly be destroyed at their providers.",
section="bridgeconf",
resource=bridgeconf,
action="destroy"
)