from __future__ import annotations import ipaddress import logging import os import re from pathlib import Path from urllib.parse import urlparse logger = logging.getLogger(__name__) ALLOWED_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".heic", ".heif", ".avif"} ALLOWED_VIDEO_EXTENSIONS = {".mp4", ".mov", ".m4v", ".webm"} ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".m4a", ".aac", ".wav", ".ogg", ".oga"} MAX_URL_LENGTH = 2000 DEFAULT_MEDIA_MAX_MB = 10 _SSRF_BLOCKED_NETWORKS = [ ipaddress.IPv4Network("127.0.0.0/8"), ipaddress.IPv4Network("10.0.0.0/8"), ipaddress.IPv4Network("172.16.0.0/12"), ipaddress.IPv4Network("192.168.0.0/16"), ipaddress.IPv4Network("169.254.0.0/16"), ipaddress.IPv6Network("::1/128"), ipaddress.IPv6Network("fc00::/7"), ipaddress.IPv6Network("fe80::/10"), ] def validate_media_url(url: str) -> bool: if not url: return False if len(url) > MAX_URL_LENGTH: logger.warning("LINE media URL too long: %d characters", len(url)) return False parsed = urlparse(url) if parsed.scheme != "https": logger.warning("LINE media URL must use HTTPS: %s", parsed.scheme) return False if not parsed.hostname: return False return _check_ssrf(parsed.hostname) def _check_ssrf(hostname: str) -> bool: try: addr = ipaddress.ip_address(hostname) except ValueError: return True for network in _SSRF_BLOCKED_NETWORKS: if addr in network: logger.warning("LINE SSRF blocked: hostname=%s network=%s", hostname, network) return False return True def detect_media_type(filename: str) -> str: ext = Path(filename).suffix.lower() if ext in ALLOWED_IMAGE_EXTENSIONS: return "image" if ext in ALLOWED_VIDEO_EXTENSIONS: return "video" if ext in ALLOWED_AUDIO_EXTENSIONS: return "audio" return "file" def validate_media_size(data: bytes, max_mb: int = DEFAULT_MEDIA_MAX_MB) -> bool: max_bytes = max_mb * 1024 * 1024 return len(data) <= max_bytes def sanitize_filename(filename: str) -> str: name = Path(filename).name name = re.sub(r'[<>:"/\\|?*]', "_", name) return name or "file"