223 lines
8.4 KiB
Python
223 lines
8.4 KiB
Python
"""
|
|
Configuration module - all constants and environment variables.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import secrets
|
|
import socket
|
|
|
|
from cryptography.fernet import Fernet
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Network Configuration
|
|
HOST = "0.0.0.0"
|
|
|
|
|
|
def _is_port_available(port):
|
|
"""Return True when the given TCP port can be bound."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
try:
|
|
sock.bind(("", port))
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _get_port_from_env(env_names, default_port):
|
|
for env_name in env_names:
|
|
value = os.environ.get(env_name)
|
|
if value:
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
logger.warning(f"Invalid port value for {env_name}: {value}")
|
|
return default_port
|
|
|
|
|
|
def _find_available_port(start_port, reserved_ports=None):
|
|
reserved = set(reserved_ports or [])
|
|
for port in range(start_port, start_port + 100):
|
|
if port in reserved:
|
|
continue
|
|
if _is_port_available(port):
|
|
return port
|
|
raise OSError(f"No available port found in range {start_port}-{start_port + 99}")
|
|
|
|
|
|
def _resolve_port(env_names, default_port, reserved_ports=None):
|
|
requested_port = _get_port_from_env(env_names, default_port)
|
|
if requested_port not in (reserved_ports or []) and _is_port_available(requested_port):
|
|
return requested_port
|
|
return _find_available_port(requested_port, reserved_ports)
|
|
|
|
# SSH Configuration
|
|
SSH_TIMEOUT = 30 # seconds
|
|
MAX_MESSAGE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
|
|
# Rate Limiting
|
|
RATE_LIMIT_WINDOW = 60 # seconds
|
|
RATE_LIMIT_MAX = 10 # max connections per IP per window
|
|
|
|
# SFTP Configuration
|
|
SFTP_CHUNK_SIZE = 64 * 1024 # 64KB chunks for transfers
|
|
|
|
|
|
def parse_size(size_str):
|
|
"""Parse size string like '50M' or '100MB' to bytes."""
|
|
size_str = size_str.upper().strip()
|
|
logger.info(f"parse_size called with: '{size_str}'")
|
|
|
|
if size_str.endswith('MB'):
|
|
result = int(float(size_str[:-2]) * 1024 * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
elif size_str.endswith('M'):
|
|
result = int(float(size_str[:-1]) * 1024 * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
elif size_str.endswith('KB'):
|
|
result = int(float(size_str[:-2]) * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
elif size_str.endswith('K'):
|
|
result = int(float(size_str[:-1]) * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
elif size_str.endswith('GB'):
|
|
result = int(float(size_str[:-2]) * 1024 * 1024 * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
elif size_str.endswith('G'):
|
|
result = int(float(size_str[:-1]) * 1024 * 1024 * 1024)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
else:
|
|
# Assume bytes if no suffix
|
|
result = int(size_str)
|
|
logger.info(f"parse_size result: {result} bytes (from {size_str})")
|
|
return result
|
|
|
|
|
|
MAX_UPLOAD_SIZE = parse_size(os.environ.get("MAX_UPLOAD_SIZE", "100M")) # 100MB default
|
|
logger.info(f"MAX_UPLOAD_SIZE loaded: {MAX_UPLOAD_SIZE} bytes ({MAX_UPLOAD_SIZE / 1024 / 1024:.1f}MB)")
|
|
|
|
# Storage Configuration
|
|
STORAGE_DIR = os.environ.get("STORAGE_DIR", "/data")
|
|
STORAGE_FILE = os.path.join(STORAGE_DIR, "credentials.db")
|
|
PORT_STATE_FILE = os.path.join(STORAGE_DIR, ".ports.json")
|
|
|
|
# Session Recording Configuration
|
|
RECORDING_ENABLED = os.environ.get("RECORDING_ENABLED", "true").lower() in ("true", "1", "yes")
|
|
RECORDING_RETENTION_DAYS = int(os.environ.get("RECORDING_RETENTION_DAYS", "30"))
|
|
RECORDINGS_PATH = os.environ.get("RECORDINGS_PATH", os.path.join(STORAGE_DIR, "recordings"))
|
|
logger.info(f"Session recording: enabled={RECORDING_ENABLED}, retention={RECORDING_RETENTION_DAYS}days, path={RECORDINGS_PATH}")
|
|
|
|
# StrixTelemetry configuration
|
|
STRIX_TELEMETRY_ENABLED = os.environ.get("STRIX_TELEMETRY_ENABLED", "true").lower() in ("true", "1", "yes")
|
|
STRIX_TELEMETRY_URL = os.environ.get("STRIX_TELEMETRY_URL", "реалния.домейн.тук").rstrip("/")
|
|
STRIX_TELEMETRY_PRODUCT = os.environ.get("STRIX_TELEMETRY_PRODUCT", "реалния-продукт-UUID-тук")
|
|
STRIX_TELEMETRY_TIMEOUT = float(os.environ.get("STRIX_TELEMETRY_TIMEOUT", "10"))
|
|
STRIX_TELEMETRY_INTERVAL = int(os.environ.get("STRIX_TELEMETRY_INTERVAL", "86400"))
|
|
|
|
# JWT Configuration
|
|
JWT_SECRET = os.environ.get("JWT_SECRET", secrets.token_hex(32))
|
|
JWT_ALGORITHM = "HS256"
|
|
JWT_EXPIRE_HOURS = 8
|
|
|
|
# Encryption Configuration for Credentials Storage
|
|
ENCRYPTION_KEY_FILE = os.path.join(STORAGE_DIR, ".encryption_key")
|
|
ENCRYPTION_KEY = os.environ.get("ENCRYPTION_KEY")
|
|
|
|
if not ENCRYPTION_KEY:
|
|
# Try to load existing key from file
|
|
if os.path.exists(ENCRYPTION_KEY_FILE):
|
|
try:
|
|
with open(ENCRYPTION_KEY_FILE, 'r') as f:
|
|
ENCRYPTION_KEY = f.read().strip()
|
|
logger.info("Loaded encryption key from file")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load encryption key from file: {e}")
|
|
|
|
if not ENCRYPTION_KEY:
|
|
# Generate a new random key
|
|
ENCRYPTION_KEY = Fernet.generate_key().decode()
|
|
try:
|
|
with open(ENCRYPTION_KEY_FILE, 'w') as f:
|
|
f.write(ENCRYPTION_KEY)
|
|
logger.info("Generated and saved new encryption key")
|
|
except Exception as e:
|
|
logger.error(f"Failed to save encryption key: {e}")
|
|
|
|
# Ensure storage directory exists (with fallback to local dir if no permission)
|
|
def ensure_storage_dir():
|
|
"""Try to create storage directory, fallback to local if no permission."""
|
|
global STORAGE_DIR, STORAGE_FILE, ENCRYPTION_KEY_FILE, RECORDINGS_PATH, PORT_STATE_FILE
|
|
|
|
try:
|
|
os.makedirs(STORAGE_DIR, exist_ok=True)
|
|
PORT_STATE_FILE = os.path.join(STORAGE_DIR, ".ports.json")
|
|
return
|
|
except (PermissionError, OSError) as e:
|
|
logger.warning(f"Cannot create storage dir {STORAGE_DIR}: {e}")
|
|
|
|
# Fallback to local directory
|
|
fallback_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
|
|
try:
|
|
os.makedirs(fallback_dir, exist_ok=True)
|
|
STORAGE_DIR = fallback_dir
|
|
STORAGE_FILE = os.path.join(STORAGE_DIR, "credentials.db")
|
|
ENCRYPTION_KEY_FILE = os.path.join(STORAGE_DIR, ".encryption_key")
|
|
RECORDINGS_PATH = os.path.join(STORAGE_DIR, "recordings")
|
|
PORT_STATE_FILE = os.path.join(STORAGE_DIR, ".ports.json")
|
|
logger.info(f"Using fallback storage directory: {STORAGE_DIR}")
|
|
except (PermissionError, OSError) as e:
|
|
logger.error(f"Cannot create fallback dir {fallback_dir}: {e}")
|
|
raise
|
|
|
|
ensure_storage_dir()
|
|
|
|
# Resolve backend ports, falling back to the next free port when defaults are busy.
|
|
WS_PORT = _resolve_port(("WEBSSH_WS_PORT",), 8081)
|
|
HTTP_PORT = _resolve_port(("WEBSSH_HTTP_PORT",), 8082, reserved_ports={WS_PORT})
|
|
SFTP_WS_PORT = _resolve_port(("WEBSSH_SFTP_PORT",), 8083, reserved_ports={WS_PORT, HTTP_PORT})
|
|
|
|
try:
|
|
with open(PORT_STATE_FILE, "w") as handle:
|
|
json.dump({
|
|
"ws_port": WS_PORT,
|
|
"http_port": HTTP_PORT,
|
|
"sftp_ws_port": SFTP_WS_PORT,
|
|
}, handle)
|
|
except Exception as exc:
|
|
logger.warning(f"Could not save selected ports to {PORT_STATE_FILE}: {exc}")
|
|
|
|
logger.info(f"Resolved backend ports: ws={WS_PORT}, http={HTTP_PORT}, sftp={SFTP_WS_PORT}")
|
|
|
|
# User Configuration (username: password_hash)
|
|
# Set via environment variable: WEBSSH_USERS=user1:pass1,user2:pass2
|
|
USERS = {}
|
|
users_env = os.environ.get("WEBSSH_USERS", "admin:admin")
|
|
for user_cred in users_env.split(","):
|
|
if ":" in user_cred:
|
|
u, p = user_cred.split(":", 1)
|
|
USERS[u.strip()] = hashlib.sha256(p.strip().encode()).hexdigest()
|
|
|
|
logger.info(f"Loaded {len(USERS)} users: {list(USERS.keys())}")
|
|
|
|
# Log storage directory status
|
|
logger.info(f"Using storage directory: {STORAGE_DIR}")
|
|
try:
|
|
import stat as stat_module
|
|
stat_info = os.stat(STORAGE_DIR)
|
|
logger.info(f"Directory permissions: uid={stat_info.st_uid}, gid={stat_info.st_gid}, mode={oct(stat_info.st_mode)}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not stat storage directory: {e}")
|