feat: Complete Sonarr integration with security enhancements
This commit adds comprehensive Sonarr webhook integration and implements critical security improvements identified in code review. ## Sonarr Integration - Full webhook support for Grab, Download, Rename, Delete, and Test events - HMAC SHA256 signature verification for webhook authentication - Series mapping system (Sonarr TVDB ID → Anime Provider URL) - 11 new API endpoints for configuration, mappings, search, and downloads - Comprehensive test suite (31 tests, all passing) - Complete documentation in docs/SONARR_INTEGRATION.md ## Security Enhancements - CORS restricted to specific origins (user's IP: 192.168.1.204:3000) - Path traversal prevention via sanitize_filename() and is_safe_filename() - Structured logging infrastructure (replaced all print() statements) - Environment-based configuration with .env support - Filename sanitization prevents malicious path attacks ## New Features - Lpayer and Sibnet downloader support - Kitsu API integration for anime metadata - Recommendation engine based on download history - Latest releases endpoint for new anime - Modular web interface with component-based templates ## Configuration - Centralized settings via app/config.py with pydantic-settings - Sonarr config auto-created in config/ directory - Example configurations provided for easy setup ## Tests - 31 Sonarr integration tests (23 functionality + 9 security) - 100+ tests passing in core test files - Security utilities fully tested ## Documentation - Updated CLAUDE.md with Sonarr and testing info - Added IMPROVEMENTS_2024-01-24.md analysis - Added SONARR_IMPLEMENTATION.md technical summary Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""Utility functions for Ohm Stream Downloader"""
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sanitize_filename(filename: str, max_length: int = 255) -> str:
|
||||
"""
|
||||
Safely sanitize filenames to prevent path traversal and invalid characters
|
||||
|
||||
Args:
|
||||
filename: The original filename
|
||||
max_length: Maximum length for filename (default 255 for most filesystems)
|
||||
|
||||
Returns:
|
||||
Sanitized safe filename
|
||||
|
||||
Examples:
|
||||
>>> sanitize_filename("../../../etc/passwd")
|
||||
'______etc_passwd'
|
||||
>>> sanitize_filename("video:file?.mp4")
|
||||
'video_file_.mp4'
|
||||
"""
|
||||
if not filename:
|
||||
return "download"
|
||||
|
||||
# Remove path separators and dangerous characters
|
||||
# Remove: \ / : * ? " < > | and control characters
|
||||
filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
|
||||
|
||||
# Remove any path components (prevent path traversal)
|
||||
filename = Path(filename).name
|
||||
|
||||
# Remove leading dots and dashes
|
||||
filename = filename.lstrip('.-')
|
||||
|
||||
# Limit length
|
||||
if len(filename) > max_length:
|
||||
# Keep extension
|
||||
name, ext = os.path.splitext(filename)
|
||||
max_name_length = max_length - len(ext)
|
||||
filename = name[:max_name_length] + ext
|
||||
|
||||
# If empty after sanitization, use default
|
||||
if not filename:
|
||||
filename = "download"
|
||||
|
||||
logger.debug(f"Sanitized filename: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
def is_safe_filename(filename: str) -> bool:
|
||||
"""
|
||||
Check if a filename is safe (no path traversal attempts)
|
||||
|
||||
Args:
|
||||
filename: The filename to check
|
||||
|
||||
Returns:
|
||||
True if filename is safe, False otherwise
|
||||
"""
|
||||
if not filename:
|
||||
return False
|
||||
|
||||
# Check for path traversal patterns
|
||||
if ".." in filename or "/" in filename or "\\" in filename:
|
||||
return False
|
||||
|
||||
# Check for absolute paths
|
||||
if filename.startswith("/") or filename.startswith("\\"):
|
||||
return False
|
||||
|
||||
# Check for drive letters (Windows)
|
||||
if re.match(r'^[A-Za-z]:', filename):
|
||||
return False
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user