Files
Maskarr/maskarr.py
Dasemu 0e5ab6af1c
All checks were successful
Build and Publish Docker Images / build-and-push (push) Successful in 3m17s
Initial commit: Maskarr - Tracker proxy for *arr apps
- HTTP proxy server to modify User-Agent and headers for private trackers
  - Web UI with collapsible tracker cards and real-time validation
  - Multi-tracker support with persistent JSON configuration
  - Thread-safe configuration management
  - Full HTTP methods support (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
  - REST API for tracker management
  - Docker support with multi-architecture builds (amd64, arm64, arm/v7)
  - CI/CD workflows for GitHub Actions and Gitea Actions
  - Automatic publishing to GitHub Container Registry and Gitea Registry
2026-01-01 13:52:11 +01:00

986 lines
33 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maskarr - Tracker Proxy</title>
<link rel="icon" type="image/x-icon" href="https://git.dariosevilla.es/repo-avatars/902a4790cab6170ef514a5d9de8e477a581e536284d6e117b3a908aed04c7ed0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1e1e1e;
color: #e0e0e0;
line-height: 1.6;
}
.header {
background: #282828;
padding: 20px;
border-bottom: 2px solid #ffa500;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 15px;
}
.header img {
width: 48px;
height: 48px;
border-radius: 8px;
}
.header-text {
flex: 1;
}
.header h1 {
color: #ffa500;
font-size: 28px;
font-weight: 600;
margin: 0;
}
.header p {
color: #b0b0b0;
margin: 5px 0 0 0;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 30px 20px;
}
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
.alert.success {
background: #2e7d32;
color: white;
border: 1px solid #4caf50;
}
.alert.error {
background: #c62828;
color: white;
border: 1px solid #f44336;
}
.alert.show {
display: block;
}
.tracker-card {
background: #282828;
border-radius: 8px;
margin-bottom: 15px;
border: 1px solid #383838;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
overflow: hidden;
}
.tracker-header {
padding: 18px 20px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
transition: background 0.2s;
}
.tracker-header:hover {
background: #2a2a2a;
}
.tracker-header-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.tracker-chevron {
color: #ffa500;
transition: transform 0.2s;
font-size: 18px;
}
.tracker-chevron.expanded {
transform: rotate(90deg);
}
.tracker-title {
color: #ffa500;
font-size: 16px;
font-weight: 600;
}
.tracker-domain {
color: #909090;
font-size: 13px;
margin-left: 15px;
}
.tracker-header-actions {
display: flex;
gap: 8px;
}
.tracker-body {
display: none;
padding: 0 20px 20px 20px;
border-top: 1px solid #383838;
}
.tracker-body.expanded {
display: block;
}
.proxy-url-box {
background: #1e1e1e;
padding: 12px;
border-radius: 4px;
margin: 15px 0;
border: 1px solid #383838;
}
.proxy-url-label {
color: #909090;
font-size: 12px;
margin-bottom: 6px;
}
.proxy-url {
background: #161616;
padding: 10px 12px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #4CAF50;
border: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.2s;
word-break: break-all;
}
.proxy-url:hover {
background: #1a1a1a;
}
.form-group {
margin-bottom: 18px;
}
label {
display: block;
margin-bottom: 6px;
color: #c0c0c0;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 12px;
background: #1e1e1e;
border: 1px solid #484848;
border-radius: 4px;
color: #e0e0e0;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus,
textarea:focus {
outline: none;
border-color: #ffa500;
}
input.error {
border-color: #d32f2f;
}
textarea {
font-family: 'Courier New', monospace;
min-height: 100px;
resize: vertical;
}
.checkbox-group {
display: flex;
align-items: center;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
margin-right: 8px;
cursor: pointer;
}
button {
padding: 10px 20px;
background: #ffa500;
color: #1e1e1e;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: background 0.2s;
}
button:hover {
background: #ff8c00;
}
button.secondary {
background: #484848;
color: #e0e0e0;
}
button.secondary:hover {
background: #585858;
}
button.danger {
background: #d32f2f;
color: white;
}
button.danger:hover {
background: #b71c1c;
}
button.small {
padding: 6px 12px;
font-size: 12px;
}
.help-text {
font-size: 12px;
color: #909090;
margin-top: 4px;
}
.help-text.error {
color: #f44336;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #707070;
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 15px;
opacity: 0.5;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #383838;
}
.new-tracker-hint {
text-align: center;
color: #909090;
font-size: 14px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="header">
<img src="https://git.dariosevilla.es/repo-avatars/902a4790cab6170ef514a5d9de8e477a581e536284d6e117b3a908aed04c7ed0" alt="Maskarr Logo">
<div class="header-text">
<h1>Maskarr</h1>
<p>Tracker Proxy for *arr Applications</p>
</div>
</div>
<div class="container">
<div id="alert" class="alert"></div>
<div id="trackersList"></div>
</div>
<script>
let trackers = {};
// Load trackers on page load
loadTrackers();
async function loadTrackers() {
try {
const response = await fetch('/api/trackers');
const data = await response.json();
trackers = data.trackers || {};
renderTrackers();
} catch (e) {
showAlert('Failed to load trackers: ' + e.message, 'error');
}
}
function renderTrackers() {
const container = document.getElementById('trackersList');
if (Object.keys(trackers).length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
</svg>
<p>No trackers configured yet. Add your first tracker below.</p>
</div>
`;
} else {
container.innerHTML = Object.entries(trackers).map(([id, config]) =>
renderTrackerCard(id, config)
).join('');
}
// Add new tracker card at the bottom
container.innerHTML += renderNewTrackerCard();
}
function renderTrackerCard(id, config) {
const proxyUrl = `${window.location.protocol}//${window.location.host}/proxy/${id}/`;
return `
<div class="tracker-card">
<div class="tracker-header" onclick="toggleTracker('${id}')">
<div class="tracker-header-left">
<span class="tracker-chevron" id="chevron-${id}">▶</span>
<span class="tracker-title">${escapeHtml(id)}</span>
<span class="tracker-domain">${escapeHtml(config.domain)}</span>
</div>
<div class="tracker-header-actions">
<button class="danger small" onclick="event.stopPropagation(); deleteTracker('${id}')">Delete</button>
</div>
</div>
<div class="tracker-body" id="body-${id}">
<div class="proxy-url-box">
<div class="proxy-url-label">Proxy URL (click to copy):</div>
<div class="proxy-url" onclick="copyToClipboard('${proxyUrl}')" title="Click to copy">
${escapeHtml(proxyUrl)}
</div>
</div>
<form onsubmit="saveTracker(event, '${id}')">
<div class="form-group">
<label for="domain-${id}">Domain *</label>
<input type="text" id="domain-${id}" value="${escapeHtml(config.domain)}" required>
<div class="help-text">Target domain without protocol (https:// is assumed)</div>
</div>
<div class="form-group">
<label for="userAgent-${id}">User-Agent *</label>
<input type="text" id="userAgent-${id}" value="${escapeHtml(config.user_agent)}" required>
<div class="help-text">Browser user-agent string to use</div>
</div>
<div class="form-group">
<label for="headers-${id}">Additional Headers (JSON)</label>
<textarea id="headers-${id}">${escapeHtml(JSON.stringify(config.headers, null, 2))}</textarea>
<div class="help-text">Optional custom headers in JSON format</div>
</div>
<div class="form-group">
<label for="timeout-${id}">Timeout (seconds)</label>
<input type="number" id="timeout-${id}" value="${config.timeout || 30}" min="1" max="120">
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="verifySSL-${id}" ${config.verify_ssl !== false ? 'checked' : ''}>
<label for="verifySSL-${id}" style="margin-bottom: 0;">Verify SSL Certificate</label>
</div>
</div>
<div class="form-actions">
<button type="submit">Save Changes</button>
</div>
</form>
</div>
</div>
`;
}
function renderNewTrackerCard() {
return `
<div class="new-tracker-hint"> Add a new tracker</div>
<div class="tracker-card">
<div class="tracker-header" onclick="toggleTracker('new')">
<div class="tracker-header-left">
<span class="tracker-chevron" id="chevron-new">▶</span>
<span class="tracker-title">New Tracker</span>
</div>
</div>
<div class="tracker-body" id="body-new">
<form onsubmit="saveTracker(event, 'new')">
<div class="form-group">
<label for="trackerId-new">Tracker ID *</label>
<input type="text" id="trackerId-new" placeholder="e.g., divteam" pattern="[a-z0-9_-]+" required>
<div class="help-text" id="help-trackerId-new">Unique identifier (lowercase letters, numbers, hyphens, underscores only)</div>
</div>
<div class="form-group">
<label for="domain-new">Domain *</label>
<input type="text" id="domain-new" placeholder="e.g., divteam.com" required>
<div class="help-text">Target domain without protocol (https:// is assumed)</div>
</div>
<div class="form-group">
<label for="userAgent-new">User-Agent *</label>
<input type="text" id="userAgent-new" placeholder="Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0" required>
<div class="help-text">Browser user-agent string to use</div>
</div>
<div class="form-group">
<label for="headers-new">Additional Headers (JSON)</label>
<textarea id="headers-new" placeholder='{"Referer": "https://example.com"}'>{}</textarea>
<div class="help-text">Optional custom headers in JSON format</div>
</div>
<div class="form-group">
<label for="timeout-new">Timeout (seconds)</label>
<input type="number" id="timeout-new" value="30" min="1" max="120">
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="verifySSL-new" checked>
<label for="verifySSL-new" style="margin-bottom: 0;">Verify SSL Certificate</label>
</div>
</div>
<div class="form-actions">
<button type="submit">Create Tracker</button>
<button type="button" class="secondary" onclick="resetNewTracker()">Clear</button>
</div>
</form>
</div>
</div>
`;
}
function toggleTracker(id) {
const body = document.getElementById(`body-${id}`);
const chevron = document.getElementById(`chevron-${id}`);
if (body.classList.contains('expanded')) {
body.classList.remove('expanded');
chevron.classList.remove('expanded');
} else {
body.classList.add('expanded');
chevron.classList.add('expanded');
}
}
async function saveTracker(event, trackerId) {
event.preventDefault();
let id;
if (trackerId === 'new') {
id = document.getElementById('trackerId-new').value.trim().toLowerCase();
// Validate tracker ID format
if (!/^[a-z0-9_-]+$/.test(id)) {
showAlert('Tracker ID must contain only lowercase letters, numbers, hyphens, and underscores', 'error');
document.getElementById('trackerId-new').classList.add('error');
document.getElementById('help-trackerId-new').classList.add('error');
return;
}
if (trackers[id]) {
showAlert(`Tracker "${id}" already exists. Please choose a different ID.`, 'error');
return;
}
} else {
id = trackerId;
}
const domain = document.getElementById(`domain-${trackerId}`).value.trim();
const userAgent = document.getElementById(`userAgent-${trackerId}`).value.trim();
const headersText = document.getElementById(`headers-${trackerId}`).value.trim() || '{}';
const timeout = parseInt(document.getElementById(`timeout-${trackerId}`).value);
const verifySSL = document.getElementById(`verifySSL-${trackerId}`).checked;
// Validate headers JSON
let headers;
try {
headers = JSON.parse(headersText);
} catch (e) {
showAlert('Invalid JSON in headers field', 'error');
return;
}
const data = {
id: id,
domain: domain,
user_agent: userAgent,
headers: headers,
timeout: timeout,
verify_ssl: verifySSL
};
try {
const response = await fetch('/api/trackers', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
showAlert(trackerId === 'new' ? 'Tracker created successfully!' : 'Tracker updated successfully!', 'success');
await loadTrackers();
} else {
showAlert(result.error || 'Failed to save tracker', 'error');
}
} catch (e) {
showAlert('Network error: ' + e.message, 'error');
}
}
async function deleteTracker(id) {
if (!confirm(`Delete tracker "${id}"?`)) return;
try {
const response = await fetch(`/api/trackers/${id}`, {method: 'DELETE'});
const result = await response.json();
if (response.ok) {
showAlert('Tracker deleted', 'success');
await loadTrackers();
} else {
showAlert(result.error || 'Failed to delete tracker', 'error');
}
} catch (e) {
showAlert('Network error: ' + e.message, 'error');
}
}
function resetNewTracker() {
document.getElementById('trackerId-new').value = '';
document.getElementById('domain-new').value = '';
document.getElementById('userAgent-new').value = '';
document.getElementById('headers-new').value = '{}';
document.getElementById('timeout-new').value = '30';
document.getElementById('verifySSL-new').checked = true;
document.getElementById('trackerId-new').classList.remove('error');
document.getElementById('help-trackerId-new').classList.remove('error');
}
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = `alert ${type} show`;
setTimeout(() => {
alert.classList.remove('show');
}, 5000);
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showAlert('URL copied to clipboard!', 'success');
}).catch(() => {
showAlert('Failed to copy URL', 'error');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Add real-time validation for tracker ID
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const trackerIdInput = document.getElementById('trackerId-new');
if (trackerIdInput) {
trackerIdInput.addEventListener('input', (e) => {
const value = e.target.value;
const helpText = document.getElementById('help-trackerId-new');
// Auto-convert to lowercase
if (value !== value.toLowerCase()) {
e.target.value = value.toLowerCase();
}
if (value && !/^[a-z0-9_-]+$/.test(value)) {
e.target.classList.add('error');
helpText.classList.add('error');
} else {
e.target.classList.remove('error');
helpText.classList.remove('error');
}
});
}
}, 100);
});
</script>
</body>
</html>
"""
def main():
"""Main entry point"""
config_manager = ConfigManager()
port = config_manager.config.get("port", 8888)
# Set config manager as class variable
MaskarrHandler.config_manager = config_manager
try:
server = HTTPServer(('0.0.0.0', port), MaskarrHandler)
print(f"🎭 Maskarr - Tracker Proxy Server")
print(f"=" * 50)
print(f"🌐 Server running on http://0.0.0.0:{port}")
print(f"📊 Web UI: http://localhost:{port}")
print(f"📝 Config file: {os.path.abspath(CONFIG_FILE)}")
print(f"=" * 50)
print(f"Press Ctrl+C to stop")
print()
server.serve_forever()
except KeyboardInterrupt:
print("\n\n👋 Shutting down gracefully...")
server.shutdown()
sys.exit(0)
except PermissionError:
print(f"❌ Error: Permission denied to bind to port {port}")
print(f"Try using a port > 1024 or run with sudo")
sys.exit(1)
except OSError as e:
print(f"❌ Error: {e}")
print(f"Port {port} might be already in use")
sys.exit(1)
except Exception as e:
print(f"❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()