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 @@ {{ pool.pool_name }} {{ pool.description }} - View/Edit + View/Edit + Copy API key {% endfor %} diff --git a/migrations/versions/a08ce5e7246a_adds_api_key_field_to_pool_table.py b/migrations/versions/a08ce5e7246a_adds_api_key_field_to_pool_table.py new file mode 100644 index 0000000..3da561c --- /dev/null +++ b/migrations/versions/a08ce5e7246a_adds_api_key_field_to_pool_table.py @@ -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')