Initial commit
Build and Push Multi-Arch Docker Image / build (push) Has been cancelled

This commit is contained in:
dido committed 2026-09-03 11:19:41 +03:00
commit 8ce01cb00a
34 files changed
+8346

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
"""
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(("STRIXWEBSSH_WS_PORT", "WEBSSH_WS_PORT"), 8081)
HTTP_PORT = _resolve_port(("STRIXWEBSSH_HTTP_PORT", "WEBSSH_HTTP_PORT"), 8082, reserved_ports={WS_PORT})
SFTP_WS_PORT = _resolve_port(("STRIXWEBSSH_SFTP_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: STRIXWEBSSH_USERS=user1:pass1,user2:pass2
USERS = {}
users_env = os.environ.get("STRIXWEBSSH_USERS", 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}")