#!/usr/bin/env python3 """ Maskarr - Tracker Proxy for *arr apps A proxy server to modify User-Agent and other headers for private trackers """ import json import os import sys from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs import requests from datetime import datetime import threading CONFIG_FILE = os.getenv("CONFIG_PATH", "maskarr_config.json") DEFAULT_CONFIG = { "trackers": {}, "port": 8888, "log_requests": True } class ConfigManager: """Thread-safe configuration manager""" def __init__(self): self.lock = threading.Lock() self.config = self.load_config() def load_config(self): """Load configuration from file""" if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, 'r') as f: return json.load(f) except Exception as e: print(f"Error loading config: {e}, using defaults") return DEFAULT_CONFIG.copy() return DEFAULT_CONFIG.copy() def save_config(self): """Save configuration to file""" with self.lock: try: with open(CONFIG_FILE, 'w') as f: json.dump(self.config, f, indent=2) return True except Exception as e: print(f"Error saving config: {e}") return False def get_tracker(self, tracker_id): """Get tracker configuration""" with self.lock: return self.config.get("trackers", {}).get(tracker_id) def add_tracker(self, tracker_id, config): """Add or update tracker configuration""" with self.lock: if "trackers" not in self.config: self.config["trackers"] = {} self.config["trackers"][tracker_id] = config return self.save_config() def delete_tracker(self, tracker_id): """Delete tracker configuration""" with self.lock: if tracker_id in self.config.get("trackers", {}): del self.config["trackers"][tracker_id] return self.save_config() def get_all_trackers(self): """Get all tracker configurations""" with self.lock: return self.config.get("trackers", {}).copy() class MaskarrHandler(BaseHTTPRequestHandler): """HTTP request handler with proxy functionality""" config_manager = None # Will be set by the server def log_message(self, format, *args): """Custom logging""" if self.config_manager.config.get("log_requests", True): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"[{timestamp}] {self.address_string()} - {format%args}") def send_json_response(self, data, status=200): """Send JSON response""" self.send_response(status) self.send_header('Content-Type', 'application/json') self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(json.dumps(data).encode()) def send_html_response(self, html, status=200): """Send HTML response""" self.send_response(status) self.send_header('Content-Type', 'text/html; charset=utf-8') self.end_headers() self.wfile.write(html.encode()) def handle_proxy_request(self, tracker_id): """Handle proxy request to tracker""" tracker_config = self.config_manager.get_tracker(tracker_id) if not tracker_config: self.send_error(404, f"Tracker '{tracker_id}' not configured") return try: # Build target URL target_domain = tracker_config.get("domain", "") if not target_domain: self.send_error(500, "Tracker domain not configured") return # Remove the tracker prefix from path path = self.path.replace(f"/proxy/{tracker_id}", "", 1) if not path: path = "/" target_url = f"https://{target_domain}{path}" # Prepare headers headers = {} # Copy custom headers from config custom_headers = tracker_config.get("headers", {}) for key, value in custom_headers.items(): headers[key] = value # Override with specific headers if "user_agent" in tracker_config: headers["User-Agent"] = tracker_config["user_agent"] # Copy certain headers from original request if not in custom headers for header in ["Cookie", "Authorization", "Accept", "Accept-Language"]: if header.lower() not in [h.lower() for h in headers.keys()]: value = self.headers.get(header) if value: headers[header] = value # Read body for POST/PUT/PATCH content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) if content_length > 0 else None # Make the request response = requests.request( method=self.command, url=target_url, headers=headers, data=body, allow_redirects=False, timeout=tracker_config.get("timeout", 30), verify=tracker_config.get("verify_ssl", True) ) # Send response back self.send_response(response.status_code) # Copy response headers (exclude problematic ones) excluded_headers = ['content-encoding', 'transfer-encoding', 'connection'] for key, value in response.headers.items(): if key.lower() not in excluded_headers: self.send_header(key, value) self.end_headers() self.wfile.write(response.content) except requests.exceptions.Timeout: self.send_error(504, "Gateway Timeout") except requests.exceptions.ConnectionError as e: self.send_error(502, f"Bad Gateway: {str(e)}") except Exception as e: print(f"Proxy error: {e}") self.send_error(500, f"Proxy error: {str(e)}") def handle_api_request(self): """Handle API requests""" path = urlparse(self.path).path query = parse_qs(urlparse(self.path).query) if path == "/api/trackers" and self.command == "GET": # Get all trackers trackers = self.config_manager.get_all_trackers() self.send_json_response({"trackers": trackers}) elif path == "/api/trackers" and self.command == "POST": # Add/update tracker try: content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) data = json.loads(body.decode()) tracker_id = data.get("id") if not tracker_id: self.send_json_response({"error": "Tracker ID required"}, 400) return tracker_config = { "domain": data.get("domain", ""), "user_agent": data.get("user_agent", ""), "headers": data.get("headers", {}), "timeout": data.get("timeout", 30), "verify_ssl": data.get("verify_ssl", True) } if self.config_manager.add_tracker(tracker_id, tracker_config): self.send_json_response({"success": True, "message": "Tracker saved"}) else: self.send_json_response({"error": "Failed to save tracker"}, 500) except json.JSONDecodeError: self.send_json_response({"error": "Invalid JSON"}, 400) except Exception as e: self.send_json_response({"error": str(e)}, 500) elif path.startswith("/api/trackers/") and self.command == "DELETE": # Delete tracker tracker_id = path.replace("/api/trackers/", "") if self.config_manager.delete_tracker(tracker_id): self.send_json_response({"success": True, "message": "Tracker deleted"}) else: self.send_json_response({"error": "Failed to delete tracker"}, 500) else: self.send_error(404, "API endpoint not found") def do_GET(self): """Handle GET requests""" self._handle_request() def do_POST(self): """Handle POST requests""" self._handle_request() def do_PUT(self): """Handle PUT requests""" self._handle_request() def do_DELETE(self): """Handle DELETE requests""" self._handle_request() def do_PATCH(self): """Handle PATCH requests""" self._handle_request() def do_HEAD(self): """Handle HEAD requests""" self._handle_request() def do_OPTIONS(self): """Handle OPTIONS requests (CORS)""" self.send_response(200) self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS') self.send_header('Access-Control-Allow-Headers', '*') self.end_headers() def _handle_request(self): """Main request router""" path = urlparse(self.path).path try: if path == "/" or path == "/index.html": self.send_html_response(get_index_html()) elif path.startswith("/api/"): self.handle_api_request() elif path.startswith("/proxy/"): # Extract tracker ID parts = path.split("/") if len(parts) >= 3: tracker_id = parts[2] self.handle_proxy_request(tracker_id) else: self.send_error(400, "Invalid proxy path") else: self.send_error(404, "Not Found") except Exception as e: print(f"Unhandled exception: {e}") import traceback traceback.print_exc() self.send_error(500, "Internal Server Error") def get_index_html(): """Generate the main HTML interface""" return """
Tracker Proxy for *arr Applications