Initial commit
Build and Push Multi-Arch Docker Image / build (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build (push) Has been cancelled
This commit is contained in:
commit
8ce01cb00a
34 files changed
+8346
No files matched your search
@@ -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}")
|
||||
Reference in new issue
Block a user