from __future__ import annotations import asyncio import ipaddress from collections.abc import AsyncIterator import boto3 import dns.asyncresolver from botocore.config import Config from botocore.exceptions import BotoCoreError, ClientError from app.contracts import Attachment from app.file_pipeline import DependencyFailure, ObjectChanged from app.url_policy import DnsError, DnsNxDomain class TrustedDnsResolver: def __init__(self, nameservers: list[str]) -> None: self._resolver = dns.asyncresolver.Resolver(configure=not nameservers) if nameservers: self._resolver.nameservers = nameservers async def resolve( self, hostname: str ) -> tuple[ipaddress.IPv4Address | ipaddress.IPv6Address, ...]: found: list[ipaddress.IPv4Address | ipaddress.IPv6Address] = [] try: for kind in ("A", "AAAA"): try: answer = await self._resolver.resolve(hostname, kind, lifetime=1.0) found.extend(ipaddress.ip_address(item.address) for item in answer) except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN as exc: raise DnsNxDomain from exc except dns.exception.DNSException as exc: raise DnsError from exc if not found: raise DnsNxDomain return tuple(found) class S3VersionReader: def __init__(self, endpoint_url: str, bucket: str, access_key: str, secret_key: str) -> None: self.bucket = bucket self.client = boto3.client( "s3", endpoint_url=endpoint_url, aws_access_key_id=access_key, aws_secret_access_key=secret_key, config=Config(s3={"addressing_style": "virtual"}, retries={"max_attempts": 2}), ) async def stream(self, attachment: Attachment) -> AsyncIterator[bytes]: try: response = await asyncio.to_thread( self.client.get_object, Bucket=self.bucket, Key=attachment.quarantine_object_key, VersionId=attachment.quarantine_version_id, IfMatch=attachment.quarantine_etag, ) body = response["Body"] while True: chunk = await asyncio.to_thread(body.read, 65_536) if not chunk: break yield chunk except ClientError as exc: code = exc.response.get("Error", {}).get("Code") if code in {"PreconditionFailed", "NoSuchKey", "NoSuchVersion"}: raise ObjectChanged("version or ETag changed") from exc raise DependencyFailure("S3 dependency unavailable") from exc except BotoCoreError as exc: raise DependencyFailure("S3 dependency unavailable") from exc