diff --git a/app/lists/__init__.py b/app/lists/__init__.py index 9b4a1f7..c1784cc 100644 --- a/app/lists/__init__.py +++ b/app/lists/__init__.py @@ -3,10 +3,12 @@ from typing import Dict, Callable, Any from app.lists.bc2 import mirror_sites from app.lists.bridgelines import bridgelines from app.lists.mirror_mapping import mirror_mapping +from app.lists.redirector import redirector_data from app.models.base import Pool lists: Dict[str, Callable[[Pool], Any]] = { "bca": mirror_mapping, "bc2": mirror_sites, "bridgelines": bridgelines, + "rdr": redirector_data, } diff --git a/app/lists/mirror_mapping.py b/app/lists/mirror_mapping.py index bd9e848..4cd04fc 100644 --- a/app/lists/mirror_mapping.py +++ b/app/lists/mirror_mapping.py @@ -35,7 +35,7 @@ class MirrorMapping(BaseModel): 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( version="1.1", mappings={ diff --git a/app/lists/redirector.py b/app/lists/redirector.py new file mode 100644 index 0000000..942db6e --- /dev/null +++ b/app/lists/redirector.py @@ -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() diff --git a/app/models/base.py b/app/models/base.py index f48e2ed..d5f8b49 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -25,6 +25,7 @@ class Group(AbstractConfiguration): class Pool(AbstractConfiguration): pool_name = db.Column(db.String(80), unique=True, nullable=False) + api_key = db.Column(db.String(80), nullable=False) @classmethod def csv_header(cls) -> List[str]: @@ -65,7 +66,8 @@ class MirrorList(AbstractConfiguration): "bc2": "Bypass Censorship v2", "bc3": "Bypass Censorship v3", "bca": "Bypass Censorship Analytics", - "bridgelines": "Tor Bridge Lines" + "bridgelines": "Tor Bridge Lines", + "rdr": "Redirector Data" } encodings_supported = { diff --git a/app/portal/list.py b/app/portal/list.py index f978b2e..b716a5a 100644 --- a/app/portal/list.py +++ b/app/portal/list.py @@ -13,6 +13,7 @@ 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.lists.redirector import redirector_data from app.models.base import MirrorList, Pool 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") if format_ == "bridgelines": 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") diff --git a/app/portal/pool.py b/app/portal/pool.py index af1cd63..a0c9207 100644 --- a/app/portal/pool.py +++ b/app/portal/pool.py @@ -1,3 +1,4 @@ +import secrets from datetime import datetime 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] 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"}) @@ -48,6 +51,7 @@ def pool_new() -> ResponseReturnValue: pool = Pool() pool.pool_name = form.group_name.data pool.description = form.description.data + pool.api_key = secrets.token_urlsafe(nbytes=32) pool.created = datetime.utcnow() pool.updated = datetime.utcnow() try: @@ -70,9 +74,13 @@ def pool_edit(pool_id: int) -> ResponseReturnValue: header="404 Pool Not Found", message="The requested pool could not be found."), status=404) - form = EditPoolForm(description=pool.description) + form = EditPoolForm(description=pool.description, + api_key=pool.api_key) if form.validate_on_submit(): 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() try: db.session.commit() diff --git a/app/portal/templates/tables.html.j2 b/app/portal/templates/tables.html.j2 index 254947d..9d5860c 100644 --- a/app/portal/templates/tables.html.j2 +++ b/app/portal/templates/tables.html.j2 @@ -367,7 +367,9 @@