feat(bridges): next generation bridge management
This commit is contained in:
parent
20fad30a06
commit
05285a4ae6
12 changed files with 329 additions and 89 deletions
|
@ -44,6 +44,15 @@ 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)
|
||||
|
|
|
@ -9,7 +9,6 @@ from app.portal.util import LifecycleForm
|
|||
|
||||
bp = Blueprint("bridge", __name__)
|
||||
|
||||
|
||||
_SECTION_TEMPLATE_VARS = {
|
||||
"section": "bridge",
|
||||
"help_url": "https://bypass.censorship.guide/user/bridges.html"
|
||||
|
@ -45,3 +44,24 @@ def bridge_blocked(bridge_id: int) -> ResponseReturnValue:
|
|||
message=bridge.hashed_fingerprint,
|
||||
form=form,
|
||||
**_SECTION_TEMPLATE_VARS)
|
||||
|
||||
|
||||
@bp.route("/expire/<bridge_id>", methods=['GET', 'POST'])
|
||||
def bridge_expire(bridge_id: int) -> ResponseReturnValue:
|
||||
bridge: Optional[Bridge] = Bridge.query.filter(Bridge.id == bridge_id, Bridge.destroyed.is_(None)).first()
|
||||
if bridge is None:
|
||||
return Response(render_template("error.html.j2",
|
||||
header="404 Proxy Not Found",
|
||||
message="The requested bridge could not be found.",
|
||||
**_SECTION_TEMPLATE_VARS))
|
||||
form = LifecycleForm()
|
||||
if form.validate_on_submit():
|
||||
bridge.destroy()
|
||||
db.session.commit()
|
||||
flash("Bridge will be shortly destroyed.", "success")
|
||||
return redirect(url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridge.conf_id))
|
||||
return render_template("lifecycle.html.j2",
|
||||
header=f"Destroy bridge {bridge.hashed_fingerprint}?",
|
||||
message=bridge.hashed_fingerprint,
|
||||
form=form,
|
||||
**_SECTION_TEMPLATE_VARS)
|
||||
|
|
|
@ -9,7 +9,7 @@ from wtforms import SelectField, StringField, IntegerField, SubmitField
|
|||
from wtforms.validators import DataRequired, NumberRange
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.base import Group
|
||||
from app.models.base import Pool
|
||||
from app.models.bridges import BridgeConf
|
||||
from app.portal.util import response_404, view_lifecycle
|
||||
|
||||
|
@ -23,17 +23,34 @@ _SECTION_TEMPLATE_VARS = {
|
|||
|
||||
|
||||
class NewBridgeConfForm(FlaskForm): # type: ignore
|
||||
provider = SelectField('Provider', validators=[DataRequired()])
|
||||
method = SelectField('Distribution Method', validators=[DataRequired()])
|
||||
description = StringField('Description')
|
||||
group = SelectField('Group', validators=[DataRequired()])
|
||||
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
||||
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."))
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
class EditBridgeConfForm(FlaskForm): # type: ignore
|
||||
description = StringField('Description')
|
||||
number = IntegerField('Number', validators=[NumberRange(1, message="One or more bridges must be created")])
|
||||
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."))
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
|
||||
|
@ -52,13 +69,7 @@ def bridgeconf_list() -> ResponseReturnValue:
|
|||
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
||||
def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
||||
form = NewBridgeConfForm()
|
||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||
form.provider.choices = [
|
||||
("aws", "AWS Lightsail"),
|
||||
("hcloud", "Hetzner Cloud"),
|
||||
("ovh", "OVH Public Cloud"),
|
||||
("gandi", "GandiCloud VPS")
|
||||
]
|
||||
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)"),
|
||||
|
@ -67,18 +78,19 @@ def bridgeconf_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
|||
("none", "None (Private)")
|
||||
]
|
||||
if form.validate_on_submit():
|
||||
bridge_conf = BridgeConf()
|
||||
bridge_conf.group_id = form.group.data
|
||||
bridge_conf.provider = form.provider.data
|
||||
bridge_conf.method = form.method.data
|
||||
bridge_conf.description = form.description.data
|
||||
bridge_conf.number = form.number.data
|
||||
bridge_conf.created = datetime.utcnow()
|
||||
bridge_conf.updated = datetime.utcnow()
|
||||
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.created = datetime.utcnow()
|
||||
bridgeconf.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.add(bridge_conf)
|
||||
db.session.add(bridgeconf)
|
||||
db.session.commit()
|
||||
flash(f"Created new bridge configuration {bridge_conf.id}.", "success")
|
||||
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")
|
||||
|
@ -100,10 +112,15 @@ def bridgeconf_edit(bridgeconf_id: int) -> ResponseReturnValue:
|
|||
**_SECTION_TEMPLATE_VARS),
|
||||
status=404)
|
||||
form = EditBridgeConfForm(description=bridgeconf.description,
|
||||
number=bridgeconf.number)
|
||||
target_number=bridgeconf.target_number,
|
||||
max_number=bridgeconf.max_number,
|
||||
expiry_hours=bridgeconf.expiry_hours,
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
bridgeconf.description = form.description.data
|
||||
bridgeconf.number = form.number.data
|
||||
bridgeconf.target_number = form.target_number.data
|
||||
bridgeconf.max_number = form.max_number.data
|
||||
bridgeconf.expiry_hours = form.expiry_hours.data
|
||||
bridgeconf.updated = datetime.utcnow()
|
||||
try:
|
||||
db.session.commit()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
<h1 class="h2 mt-3">Tor Bridge Configuration</h1>
|
||||
<h2 class="h3">{{ bridgeconf.group.group_name }}: {{ bridgeconf.provider }}/{{ bridgeconf.method }}</h2>
|
||||
<h2 class="h3">{{ bridgeconf.pool.pool_name }}: {{ bridgeconf.method }}</h2>
|
||||
|
||||
<div style="border: 1px solid #666;" class="p-3">
|
||||
{{ render_form(form) }}
|
||||
|
|
|
@ -453,10 +453,11 @@
|
|||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">Pool</th>
|
||||
<th scope="col">Distribution Method</th>
|
||||
<th scope="col">Number</th>
|
||||
<th scope="col">Target Number</th>
|
||||
<th scope="col">Max Number</th>
|
||||
<th scope="col">Expiry Timer</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -465,11 +466,12 @@
|
|||
{% if not bridgeconf.destroyed %}
|
||||
<tr class="align-middle">
|
||||
<td>
|
||||
<a href="{{ url_for("portal.group.group_edit", group_id=bridgeconf.group.id) }}">{{ bridgeconf.group.group_name }}</a>
|
||||
<a href="{{ url_for("portal.pool.pool_edit", pool_id=bridgeconf.pool.id) }}" title="{{ bridgeconf.pool.description }}">{{ bridgeconf.pool.pool_name }}</a>
|
||||
</td>
|
||||
<td>{{ bridgeconf.provider }}</td>
|
||||
<td>{{ bridgeconf.method }}</td>
|
||||
<td>{{ bridgeconf.number }}</td>
|
||||
<td>{{ bridgeconf.target_number }}</td>
|
||||
<td>{{ bridgeconf.max_number }}</td>
|
||||
<td>{{ bridgeconf.expiry_hours }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for("portal.bridgeconf.bridgeconf_edit", bridgeconf_id=bridgeconf.id) }}"
|
||||
class="btn btn-primary btn-sm">View/Edit</a>
|
||||
|
@ -489,7 +491,7 @@
|
|||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Group</th>
|
||||
<th scope="col">Pool</th>
|
||||
<th scope="col">Configuration</th>
|
||||
<th scope="col">Nickname</th>
|
||||
<th scope="col">Hashed Fingerprint</th>
|
||||
|
@ -502,9 +504,9 @@
|
|||
{% if not bridge.destroyed %}
|
||||
<tr class="align-middle{% if bridge.deprecated %} bg-warning{% endif %}">
|
||||
<td>
|
||||
<a href="{{ url_for("portal.group.group_edit", group_id=bridge.conf.group.id) }}">{{ bridge.conf.group.group_name }}</a>
|
||||
<a href="{{ url_for("portal.pool.pool_edit", pool_id=bridge.conf.pool_id) }}">{{ bridge.conf.pool.pool_name }}</a>
|
||||
</td>
|
||||
<td>{{ bridge.conf.description }} ({{ bridge.conf.provider }}/{{ bridge.conf.method }})</td>
|
||||
<td>{{ bridge.conf.description }} ({{ bridge.provider }}/{{ bridge.conf.method }})</td>
|
||||
<td>
|
||||
<a href="https://metrics.torproject.org/rs.html#details/{{ bridge.hashed_fingerprint }}">
|
||||
{{ bridge.nickname }}
|
||||
|
@ -541,7 +543,9 @@
|
|||
<td>
|
||||
{% if bridge.deprecated %}
|
||||
<a href="#" class="disabled btn btn-sm btn-outline-dark">Expiring
|
||||
in {{ bridge.deprecated | mirror_expiry }}</a>
|
||||
in {{ bridge | bridge_expiry }}</a>
|
||||
<a href="{{ url_for("portal.bridge.bridge_expire", bridge_id=bridge.id) }}"
|
||||
class="btn btn-danger btn-sm">Expire</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for("portal.bridge.bridge_blocked", bridge_id=bridge.id) }}"
|
||||
class="btn btn-warning btn-sm">Mark blocked</a>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue