block/bridge: refactor into general bridge block subsystem
This commit is contained in:
parent
51092865a2
commit
ca3fc844b7
7 changed files with 148 additions and 46 deletions
|
@ -9,7 +9,7 @@ from app.extensions import db
|
||||||
from app.models.activity import Activity
|
from app.models.activity import Activity
|
||||||
from app.models.automation import Automation, AutomationState, AutomationLogs
|
from app.models.automation import Automation, AutomationState, AutomationLogs
|
||||||
from app.terraform import BaseAutomation
|
from app.terraform import BaseAutomation
|
||||||
from app.terraform.block_bridge_github import BlockBridgeGitHubAutomation
|
from app.terraform.block.bridge_github import BlockBridgeGitHubAutomation
|
||||||
from app.terraform.block_external import BlockExternalAutomation
|
from app.terraform.block_external import BlockExternalAutomation
|
||||||
from app.terraform.block_ooni import BlockOONIAutomation
|
from app.terraform.block_ooni import BlockOONIAutomation
|
||||||
from app.terraform.block_roskomsvoboda import BlockRoskomsvobodaAutomation
|
from app.terraform.block_roskomsvoboda import BlockRoskomsvobodaAutomation
|
||||||
|
|
|
@ -10,7 +10,6 @@ class BridgeConf(AbstractConfiguration):
|
||||||
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
|
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
|
||||||
provider = db.Column(db.String(20), nullable=False)
|
provider = db.Column(db.String(20), nullable=False)
|
||||||
method = db.Column(db.String(20), nullable=False)
|
method = db.Column(db.String(20), nullable=False)
|
||||||
description = db.Column(db.String(255))
|
|
||||||
number = db.Column(db.Integer())
|
number = db.Column(db.Integer())
|
||||||
|
|
||||||
group = db.relationship("Group", back_populates="bridgeconfs")
|
group = db.relationship("Group", back_populates="bridgeconfs")
|
||||||
|
|
0
app/terraform/block/__init__.py
Normal file
0
app/terraform/block/__init__.py
Normal file
101
app/terraform/block/bridge.py
Normal file
101
app/terraform/block/bridge.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from abc import abstractmethod
|
||||||
|
import fnmatch
|
||||||
|
from typing import Tuple, List, Callable
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.activity import Activity
|
||||||
|
from app.models.bridges import Bridge
|
||||||
|
from app.models.mirrors import Proxy
|
||||||
|
from app.terraform import BaseAutomation
|
||||||
|
|
||||||
|
|
||||||
|
class BlockBridgeAutomation(BaseAutomation):
|
||||||
|
patterns: List[str]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Constructor method.
|
||||||
|
"""
|
||||||
|
self.ips = []
|
||||||
|
self.fingerprints = []
|
||||||
|
self.hashed_fingerprints = []
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def perform_deprecations(self, ids: List[str], bridge_select_func: Callable[[str], Bridge]
|
||||||
|
) -> List[Tuple[str, str]]:
|
||||||
|
rotated = []
|
||||||
|
for id_ in ids:
|
||||||
|
bridge = bridge_select_func(id_)
|
||||||
|
logging.debug("Found %s blocked", bridge.fingerprint)
|
||||||
|
if bridge.added > datetime.utcnow() - timedelta(hours=3):
|
||||||
|
logging.debug("Not rotating a bridge less than 3 hours old")
|
||||||
|
continue
|
||||||
|
if bridge.deprecate(reason=self.short_name):
|
||||||
|
logging.info("Rotated %s", bridge.fingerprint)
|
||||||
|
rotated.append((bridge.fingerprint, bridge.conf.provider))
|
||||||
|
else:
|
||||||
|
logging.debug("Not rotating a bridge that is already deprecated")
|
||||||
|
return rotated
|
||||||
|
|
||||||
|
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
||||||
|
self.fetch()
|
||||||
|
logging.debug("Fetch complete")
|
||||||
|
self.parse()
|
||||||
|
logging.debug("Parse complete")
|
||||||
|
rotated = []
|
||||||
|
rotated.extend(self.perform_deprecations(self.ips, get_bridge_by_ip))
|
||||||
|
rotated.extend(self.perform_deprecations(self.fingerprints, get_bridge_by_fingerprint))
|
||||||
|
rotated.extend(self.perform_deprecations(self.fingerprints, get_bridge_by_hashed_fingerprint))
|
||||||
|
if rotated:
|
||||||
|
activity = Activity(
|
||||||
|
activity_type="block",
|
||||||
|
text=(f"[{self.short_name}] ♻ Rotated {len(rotated)} bridges: \n"
|
||||||
|
+ "\n".join([f"* {fingerprint} ({provider})" for fingerprint, provider in rotated]))
|
||||||
|
)
|
||||||
|
db.session.add(activity)
|
||||||
|
activity.notify()
|
||||||
|
db.session.commit()
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fetch(self) -> None:
|
||||||
|
"""
|
||||||
|
Fetch the blocklist data. It is the responsibility of the automation task
|
||||||
|
to persist this within the object for the parse step.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse(self) -> None:
|
||||||
|
"""
|
||||||
|
Parse the blocklist data.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_bridge_by_ip(ip: str) -> Bridge:
|
||||||
|
return Bridge.query.filter( # type: ignore[no-any-return]
|
||||||
|
Bridge.deprecated.is_(None),
|
||||||
|
Bridge.destroyed.is_(None),
|
||||||
|
Bridge.bridgeline.contains(f" {ip} ")
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
def get_bridge_by_fingerprint(fingerprint: str) -> Bridge:
|
||||||
|
return Bridge.query.filter( # type: ignore[no-any-return]
|
||||||
|
Bridge.deprecated.is_(None),
|
||||||
|
Bridge.destroyed.is_(None),
|
||||||
|
Bridge.fingerprint == fingerprint
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
def get_bridge_by_hashed_fingerprint(hashed_fingerprint: str) -> Bridge:
|
||||||
|
return Bridge.query.filter( # type: ignore[no-any-return]
|
||||||
|
Bridge.deprecated.is_(None),
|
||||||
|
Bridge.destroyed.is_(None),
|
||||||
|
Bridge.hashed_fingerprint == hashed_fingerprint
|
||||||
|
).first()
|
25
app/terraform/block/bridge_github.py
Normal file
25
app/terraform/block/bridge_github.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from flask import current_app
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
from app.terraform.block.bridge_reachability import BlockBridgeReachabilityAutomation
|
||||||
|
|
||||||
|
|
||||||
|
class BlockBridgeGitHubAutomation(BlockBridgeReachabilityAutomation):
|
||||||
|
"""
|
||||||
|
Automation task to import bridge reachability results from GitHub.
|
||||||
|
"""
|
||||||
|
|
||||||
|
short_name = "block_bridge_github"
|
||||||
|
description = "Import bridge reachability results from GitHub"
|
||||||
|
frequency = 30
|
||||||
|
|
||||||
|
def fetch(self) -> None:
|
||||||
|
github = Github(current_app.config['GITHUB_API_KEY'])
|
||||||
|
repo = github.get_repo(current_app.config['GITHUB_BRIDGE_REPO'])
|
||||||
|
for vantage_point in current_app.config['GITHUB_BRIDGE_VANTAGE_POINTS']:
|
||||||
|
contents = repo.get_contents(f"recentResult_{vantage_point}")
|
||||||
|
if isinstance(contents, list):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Expected a file at recentResult_{vantage_point}"
|
||||||
|
" but got a directory.")
|
||||||
|
self._lines = contents.decoded_content.decode('utf-8').splitlines()
|
21
app/terraform/block/bridge_reachability.py
Normal file
21
app/terraform/block/bridge_reachability.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from dateutil.parser import isoparse
|
||||||
|
|
||||||
|
from app.terraform.block.bridge import BlockBridgeAutomation
|
||||||
|
|
||||||
|
|
||||||
|
class BlockBridgeReachabilityAutomation(BlockBridgeAutomation):
|
||||||
|
|
||||||
|
_lines: List[str]
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
for line in self._lines:
|
||||||
|
parts = line.split("\t")
|
||||||
|
if isoparse(parts[2]) < (datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
- datetime.timedelta(days=3)):
|
||||||
|
# Skip results older than 3 days
|
||||||
|
continue
|
||||||
|
if int(parts[1]) < 40:
|
||||||
|
self.hashed_fingerprints.append(parts[0])
|
|
@ -1,44 +0,0 @@
|
||||||
import datetime
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from dateutil.parser import isoparse
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
from app import app
|
|
||||||
from app.extensions import db
|
|
||||||
from app.models.bridges import Bridge
|
|
||||||
from app.terraform import BaseAutomation
|
|
||||||
|
|
||||||
|
|
||||||
class BlockBridgeGitHubAutomation(BaseAutomation):
|
|
||||||
"""
|
|
||||||
Automation task to import bridge reachability results from GitHub.
|
|
||||||
"""
|
|
||||||
|
|
||||||
short_name = "block_bridge_github"
|
|
||||||
description = "Import bridge reachability results from GitHub"
|
|
||||||
frequency = 30
|
|
||||||
|
|
||||||
def automate(self, full: bool = False) -> Tuple[bool, str]:
|
|
||||||
github = Github(app.config['GITHUB_API_KEY'])
|
|
||||||
repo = github.get_repo(app.config['GITHUB_BRIDGE_REPO'])
|
|
||||||
for vantage_point in app.config['GITHUB_BRIDGE_VANTAGE_POINTS']:
|
|
||||||
contents = repo.get_contents(f"recentResult_{vantage_point}")
|
|
||||||
if isinstance(contents, list):
|
|
||||||
return (False,
|
|
||||||
f"Expected a file at recentResult_{vantage_point}"
|
|
||||||
" but got a directory.")
|
|
||||||
results = contents.decoded_content.decode('utf-8').splitlines()
|
|
||||||
for result in results:
|
|
||||||
parts = result.split("\t")
|
|
||||||
if isoparse(parts[2]) < (datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
- datetime.timedelta(days=3)):
|
|
||||||
continue
|
|
||||||
if int(parts[1]) < 40:
|
|
||||||
bridge: Bridge = Bridge.query.filter(
|
|
||||||
Bridge.hashed_fingerprint == parts[0]
|
|
||||||
).first()
|
|
||||||
if bridge is not None:
|
|
||||||
bridge.deprecate(reason="github")
|
|
||||||
db.session.commit()
|
|
||||||
return True, ""
|
|
Loading…
Add table
Add a link
Reference in a new issue