""" 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('M') or size_str.endswith('MB'): # Remove 'M' or 'MB' and convert to bytes 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('K') or size_str.endswith('KB'): # Remove 'K' or 'KB' and convert to bytes result = int(float(size_str[:-1]) * 1024) logger.info(f"parse_size result: {result} bytes (from {size_str})") return result elif size_str.endswith('G') or size_str.endswith('GB'): # Remove 'G' or 'GB' and convert to bytes 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}") # 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}")