Start supporting a server blocklist

This commit is contained in:
Thomas Sileo 2022-08-15 10:15:00 +02:00
parent 2d2b2e5873
commit 1e6a290fb3
6 changed files with 45 additions and 0 deletions

View file

@ -59,6 +59,8 @@ class ObjectNotFoundError(Exception):
class FetchErrorTypeEnum(str, enum.Enum):
TIMEOUT = "TIMEOUT"
NOT_FOUND = "NOT_FOUND"
UNAUHTORIZED = "UNAUTHORIZED"
INTERNAL_ERROR = "INTERNAL_ERROR"

View file

@ -114,6 +114,10 @@ class Actor:
def attachments(self) -> list[ap.RawObject]:
return ap.as_list(self.ap_actor.get("attachment", []))
@cached_property
def server(self) -> str:
return urlparse(self.ap_id).netloc
class RemoteActor(Actor):
def __init__(self, ap_actor: ap.RawObject) -> None:

View file

@ -26,6 +26,7 @@ from app.actor import fetch_actor
from app.actor import save_actor
from app.ap_object import RemoteObject
from app.config import BASE_URL
from app.config import BLOCKED_SERVERS
from app.config import ID
from app.config import MANUALLY_APPROVES_FOLLOWERS
from app.database import AsyncSession
@ -1447,6 +1448,10 @@ async def save_to_inbox(
logger.exception("Failed to fetch actor")
return
if actor.server in BLOCKED_SERVERS:
logger.warning(f"Server {actor.server} is blocked")
return
if "id" not in raw_object:
await _process_transient_object(db_session, raw_object, actor)
return None

View file

@ -50,6 +50,11 @@ class _ProfileMetadata(pydantic.BaseModel):
value: str
class _BlockedServer(pydantic.BaseModel):
hostname: str
reason: str | None = None
class Config(pydantic.BaseModel):
domain: str
username: str
@ -65,6 +70,7 @@ class Config(pydantic.BaseModel):
privacy_replace: list[_PrivacyReplace] | None = None
metadata: list[_ProfileMetadata] | None = None
code_highlighting_theme = "friendly_grayscale"
blocked_servers: list[_BlockedServer] = []
# Config items to make tests easier
sqlalchemy_database: str | None = None
@ -109,6 +115,9 @@ MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
PRIVACY_REPLACE = None
if CONFIG.privacy_replace:
PRIVACY_REPLACE = {pr.domain: pr.replace_by for pr in CONFIG.privacy_replace}
BLOCKED_SERVERS = {blocked_server.hostname for blocked_server in CONFIG.blocked_servers}
BASE_URL = ID
DEBUG = CONFIG.debug
DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db"

View file

@ -9,6 +9,7 @@ from typing import Any
from typing import Dict
from typing import MutableMapping
from typing import Optional
from urllib.parse import urlparse
import fastapi
import httpx
@ -21,6 +22,7 @@ from sqlalchemy import select
from app import activitypub as ap
from app import config
from app.config import BLOCKED_SERVERS
from app.config import KEY_PATH
from app.database import AsyncSession
from app.database import get_db_session
@ -144,6 +146,7 @@ class HTTPSigInfo:
is_ap_actor_gone: bool = False
is_unsupported_algorithm: bool = False
is_expired: bool = False
server: str | None = None
async def httpsig_checker(
@ -157,11 +160,22 @@ async def httpsig_checker(
logger.info("No HTTP signature found")
return HTTPSigInfo(has_valid_signature=False)
try:
key_id = hsig["keyId"]
except KeyError:
logger.info("Missing keyId")
return HTTPSigInfo(
has_valid_signature=False,
)
server = urlparse(key_id).hostname
if alg := hsig.get("algorithm") not in ["rsa-sha256", "hs2019"]:
logger.info(f"Unsupported HTTP sig algorithm: {alg}")
return HTTPSigInfo(
has_valid_signature=False,
is_unsupported_algorithm=True,
server=server,
)
logger.debug(f"hsig={hsig}")
@ -180,6 +194,7 @@ async def httpsig_checker(
return HTTPSigInfo(
has_valid_signature=False,
is_expired=True,
server=server,
)
try:
@ -196,6 +211,7 @@ async def httpsig_checker(
signed_string, base64.b64decode(hsig["signature"]), k.pubkey
),
signed_by_ap_actor_id=k.owner,
server=server,
)
logger.info(f"Valid HTTP signature for {httpsig_info.signed_by_ap_actor_id}")
return httpsig_info
@ -206,6 +222,10 @@ async def enforce_httpsig(
httpsig_info: HTTPSigInfo = fastapi.Depends(httpsig_checker),
) -> HTTPSigInfo:
"""FastAPI Depends"""
if httpsig_info.server in BLOCKED_SERVERS:
logger.warning(f"{httpsig_info.server} is blocked")
raise fastapi.HTTPException(status_code=403, detail="Blocked")
if not httpsig_info.has_valid_signature:
logger.warning(f"Invalid HTTP sig {httpsig_info=}")
body = await request.body()

View file

@ -5,6 +5,7 @@ from urllib.parse import urlparse
from loguru import logger
from app.config import BLOCKED_SERVERS
from app.config import DEBUG
@ -53,6 +54,10 @@ def is_url_valid(url: str) -> bool:
if not parsed.hostname or parsed.hostname.lower() in ["localhost"]:
return False
if parsed.hostname in BLOCKED_SERVERS:
logger.warning(f"{parsed.hostname} is blocked")
return False
ip_address = _getaddrinfo(
parsed.hostname, parsed.port or (80 if parsed.scheme == "http" else 443)
)