feat(lists): adds redirector data format

This commit is contained in:
Iain Learmonth 2022-12-21 19:27:46 +00:00
parent c16a80dc16
commit 60255afe3f
8 changed files with 117 additions and 4 deletions

View file

@ -3,10 +3,12 @@ from typing import Dict, Callable, Any
from app.lists.bc2 import mirror_sites from app.lists.bc2 import mirror_sites
from app.lists.bridgelines import bridgelines from app.lists.bridgelines import bridgelines
from app.lists.mirror_mapping import mirror_mapping from app.lists.mirror_mapping import mirror_mapping
from app.lists.redirector import redirector_data
from app.models.base import Pool from app.models.base import Pool
lists: Dict[str, Callable[[Pool], Any]] = { lists: Dict[str, Callable[[Pool], Any]] = {
"bca": mirror_mapping, "bca": mirror_mapping,
"bc2": mirror_sites, "bc2": mirror_sites,
"bridgelines": bridgelines, "bridgelines": bridgelines,
"rdr": redirector_data,
} }

View file

@ -35,7 +35,7 @@ class MirrorMapping(BaseModel):
title = "Mirror Mapping Version 1.1" title = "Mirror Mapping Version 1.1"
def mirror_mapping(pool: Pool) -> Dict[str, Union[str, Dict[str, str]]]: def mirror_mapping(ignored_pool: Pool) -> Dict[str, Union[str, Dict[str, str]]]:
return MirrorMapping( return MirrorMapping(
version="1.1", version="1.1",
mappings={ mappings={

51
app/lists/redirector.py Normal file
View file

@ -0,0 +1,51 @@
from typing import List, Dict, Union, Optional
from pydantic import BaseModel
from app.models.base import Pool
from app.models.mirrors import Proxy
class RedirectorPool(BaseModel):
short_name: str
description: str
api_key: str
origins: Dict[str, str]
class RedirectorData(BaseModel):
version: str
pools: List[RedirectorPool]
def redirector_pool_origins(pool: Pool) -> Dict[str, str]:
origins: Dict[str, str] = dict()
active_proxies = Proxy.query.filter(
Proxy.deprecated.is_(None),
Proxy.destroyed.is_(None),
Proxy.pool_id == pool.id
)
for proxy in active_proxies:
origins[proxy.origin.domain_name] = proxy.url
return origins
def redirector_pool(pool: Pool) -> RedirectorPool:
return RedirectorPool(
short_name=pool.pool_name,
description=pool.description,
api_key=pool.api_key,
origins=redirector_pool_origins(pool)
)
def redirector_data(ignored_pool: Optional[Pool]) -> Dict[str, Union[str, Dict[str, Union[Dict[str, str]]]]]:
active_pools = Pool.query.filter(
Pool.destroyed.is_(None)
).all()
return RedirectorData(
version="1.0",
pools=[
redirector_pool(pool) for pool in active_pools
]
).dict()

View file

@ -25,6 +25,7 @@ class Group(AbstractConfiguration):
class Pool(AbstractConfiguration): class Pool(AbstractConfiguration):
pool_name = db.Column(db.String(80), unique=True, nullable=False) pool_name = db.Column(db.String(80), unique=True, nullable=False)
api_key = db.Column(db.String(80), nullable=False)
@classmethod @classmethod
def csv_header(cls) -> List[str]: def csv_header(cls) -> List[str]:
@ -65,7 +66,8 @@ class MirrorList(AbstractConfiguration):
"bc2": "Bypass Censorship v2", "bc2": "Bypass Censorship v2",
"bc3": "Bypass Censorship v3", "bc3": "Bypass Censorship v3",
"bca": "Bypass Censorship Analytics", "bca": "Bypass Censorship Analytics",
"bridgelines": "Tor Bridge Lines" "bridgelines": "Tor Bridge Lines",
"rdr": "Redirector Data"
} }
encodings_supported = { encodings_supported = {

View file

@ -13,6 +13,7 @@ from app.extensions import db
from app.lists.bc2 import mirror_sites from app.lists.bc2 import mirror_sites
from app.lists.bridgelines import bridgelines from app.lists.bridgelines import bridgelines
from app.lists.mirror_mapping import mirror_mapping from app.lists.mirror_mapping import mirror_mapping
from app.lists.redirector import redirector_data
from app.models.base import MirrorList, Pool from app.models.base import MirrorList, Pool
from app.portal.util import response_404, view_lifecycle from app.portal.util import response_404, view_lifecycle
@ -63,6 +64,8 @@ def list_preview(format_: str, pool_id: int) -> ResponseReturnValue:
return Response(json.dumps(mirror_sites(pool)), content_type="application/json") return Response(json.dumps(mirror_sites(pool)), content_type="application/json")
if format_ == "bridgelines": if format_ == "bridgelines":
return Response(json.dumps(bridgelines(pool)), content_type="application/json") return Response(json.dumps(bridgelines(pool)), content_type="application/json")
if format_ == "rdr":
return Response(json.dumps(redirector_data(pool)), content_type="application/json")
return response_404(message="Format not found") return response_404(message="Format not found")

View file

@ -1,3 +1,4 @@
import secrets
from datetime import datetime from datetime import datetime
from flask import render_template, url_for, flash, redirect, Response, Blueprint from flask import render_template, url_for, flash, redirect, Response, Blueprint
@ -22,6 +23,8 @@ class NewPoolForm(FlaskForm): # type: ignore[misc]
class EditPoolForm(FlaskForm): # type: ignore[misc] class EditPoolForm(FlaskForm): # type: ignore[misc]
description = StringField("Description", validators=[DataRequired()]) description = StringField("Description", validators=[DataRequired()])
api_key = StringField("API Key", description=("Any change to this field (e.g. clearing it) will result in the "
"API key being regenerated."))
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"}) submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
@ -48,6 +51,7 @@ def pool_new() -> ResponseReturnValue:
pool = Pool() pool = Pool()
pool.pool_name = form.group_name.data pool.pool_name = form.group_name.data
pool.description = form.description.data pool.description = form.description.data
pool.api_key = secrets.token_urlsafe(nbytes=32)
pool.created = datetime.utcnow() pool.created = datetime.utcnow()
pool.updated = datetime.utcnow() pool.updated = datetime.utcnow()
try: try:
@ -70,9 +74,13 @@ def pool_edit(pool_id: int) -> ResponseReturnValue:
header="404 Pool Not Found", header="404 Pool Not Found",
message="The requested pool could not be found."), message="The requested pool could not be found."),
status=404) status=404)
form = EditPoolForm(description=pool.description) form = EditPoolForm(description=pool.description,
api_key=pool.api_key)
if form.validate_on_submit(): if form.validate_on_submit():
pool.description = form.description.data pool.description = form.description.data
if form.api_key.data != pool.api_key:
pool.api_key = secrets.token_urlsafe(nbytes=32)
form.api_key.data = pool.api_key
pool.updated = datetime.utcnow() pool.updated = datetime.utcnow()
try: try:
db.session.commit() db.session.commit()

View file

@ -367,7 +367,9 @@
<tr> <tr>
<td>{{ pool.pool_name }}</td> <td>{{ pool.pool_name }}</td>
<td>{{ pool.description }}</td> <td>{{ pool.description }}</td>
<td><a href="{{ url_for("portal.pool.pool_edit", pool_id=pool.id) }}" class="btn btn-primary btn-sm">View/Edit</a></td> <td><a href="{{ url_for("portal.pool.pool_edit", pool_id=pool.id) }}" class="btn btn-primary btn-sm">View/Edit</a>
<a href="javascript:navigator.clipboard.writeText('{{ pool.api_key }}');"
class="btn btn-sm btn-outline-dark">Copy API key</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -0,0 +1,45 @@
"""Adds api key field to pool table
Revision ID: a08ce5e7246a
Revises: 6a59928efeb7
Create Date: 2022-12-20 18:10:19.540534
"""
import secrets
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import Session
from app.models.base import Pool
revision = 'a08ce5e7246a'
down_revision = '6a59928efeb7'
branch_labels = None
depends_on = None
# class Pool(db.Model):
# id = db.Column(db.Integer, primary_key=True)
# description = db.Column(db.String(255), nullable=False)
# added = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False)
# updated = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False)
# destroyed = db.Column(db.DateTime(), nullable=True)
# pool_name = db.Column(db.String(80), unique=True, nullable=False)
# api_key = db.Column(db.String(80), nullable=False)
def upgrade():
with op.batch_alter_table('pool', schema=None) as batch_op:
batch_op.add_column(sa.Column('api_key', sa.String(length=80), nullable=True, unique=False))
session = Session(bind=op.get_bind())
for pool in session.query(Pool).all():
pool.api_key = secrets.token_urlsafe(nbytes=32)
session.commit()
with op.batch_alter_table('pool', schema=None) as batch_op:
batch_op.alter_column('api_key', nullable=False, unique=True)
def downgrade():
with op.batch_alter_table('pool', schema=None) as batch_op:
batch_op.drop_column('api_key')