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

+458
View File
@@ -0,0 +1,458 @@
"""
Database module - CredentialsManager for SQLite operations.
"""
import os
import sqlite3
from typing import Any, Dict, List, Optional
from cryptography.fernet import Fernet
from .config import ENCRYPTION_KEY, STORAGE_FILE, logger
class CredentialsManager:
"""Manages encrypted storage of SSH credentials per user."""
def __init__(self, db_path: str, encryption_key: str):
self.db_path = db_path
self.cipher = Fernet(encryption_key.encode() if isinstance(encryption_key, str) else encryption_key)
# Ensure directory exists before initializing DB
db_dir = os.path.dirname(db_path)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
self._init_db()
def _init_db(self):
"""Initialize SQLite database."""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS connections (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL,
ssh_username TEXT NOT NULL,
auth_type TEXT NOT NULL,
password_encrypted BLOB,
key_encrypted BLOB,
ws_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
# Migration: Create recordings table if not exists
cursor.execute('''
CREATE TABLE IF NOT EXISTS recordings (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
connection_id TEXT NOT NULL,
connection_name TEXT,
session_id TEXT NOT NULL,
host TEXT NOT NULL,
ssh_username TEXT NOT NULL,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
duration_seconds INTEGER,
file_size_bytes INTEGER,
file_path TEXT NOT NULL,
width INTEGER DEFAULT 80,
height INTEGER DEFAULT 24,
FOREIGN KEY (connection_id) REFERENCES connections(id)
)
''')
# Migration: Add connection_name column if it doesn't exist (for existing databases)
try:
cursor.execute('ALTER TABLE recordings ADD COLUMN connection_name TEXT')
logger.info("Added connection_name column to recordings table")
except sqlite3.OperationalError:
# Column already exists
pass
cursor.execute('CREATE INDEX IF NOT EXISTS idx_recordings_username ON recordings(username)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_recordings_connection ON recordings(connection_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_recordings_started_at ON recordings(started_at)')
# Migration: Add lock_timeout and disconnect_timeout columns if they don't exist
try:
cursor.execute('ALTER TABLE connections ADD COLUMN lock_timeout INTEGER DEFAULT 15')
logger.info("Added lock_timeout column to connections table")
except sqlite3.OperationalError:
pass
try:
cursor.execute('ALTER TABLE connections ADD COLUMN disconnect_timeout INTEGER DEFAULT 60')
logger.info("Added disconnect_timeout column to connections table")
except sqlite3.OperationalError:
pass
conn.commit()
conn.close()
logger.info(f"Credentials database initialized at {self.db_path}")
def _encrypt(self, data: str) -> bytes:
"""Encrypt string data."""
if not data:
return None
return self.cipher.encrypt(data.encode())
def _decrypt(self, data: bytes) -> str:
"""Decrypt bytes to string."""
if not data:
return None
try:
return self.cipher.decrypt(data).decode()
except Exception as e:
logger.warning(f"Failed to decrypt data: {e}")
return None
def save_connection(self, username: str, conn_data: Dict[str, Any]) -> bool:
"""Save or update a connection."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
conn_id = conn_data.get('id')
name = conn_data.get('name', 'Unnamed')
host = conn_data.get('host', '')
port = conn_data.get('port', 22)
ssh_username = conn_data.get('username', '')
auth_type = conn_data.get('authType', 'password')
ws_url = conn_data.get('wsUrl', 'ws://localhost:2222')
lock_timeout = conn_data.get('lockTimeout', 15)
disconnect_timeout = conn_data.get('disconnectTimeout', 60)
# Encrypt sensitive data
password = conn_data.get('password', '')
key = conn_data.get('key', '')
password_encrypted = self._encrypt(password) if password else None
key_encrypted = self._encrypt(key) if key else None
cursor.execute('''
INSERT OR REPLACE INTO connections
(id, username, name, host, port, ssh_username, auth_type,
password_encrypted, key_encrypted, ws_url, lock_timeout, disconnect_timeout, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (conn_id, username, name, host, port, ssh_username, auth_type,
password_encrypted, key_encrypted, ws_url, lock_timeout, disconnect_timeout))
conn.commit()
conn.close()
logger.info(f"Saved connection {name} for user {username}")
return True
except Exception as e:
logger.error(f"Failed to save connection: {e}")
return False
def get_connections(self, username: str) -> list:
"""Get all connections for a user."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
logger.info(f"Querying connections for username: '{username}'") # DEBUG
# First, let's see all connections to debug
cursor.execute('SELECT DISTINCT username FROM connections')
all_users = cursor.fetchall()
logger.info(f"All users in DB: {all_users}") # DEBUG
cursor.execute('''
SELECT id, name, host, port, ssh_username, auth_type,
password_encrypted, key_encrypted, ws_url, lock_timeout, disconnect_timeout
FROM connections WHERE username = ?
''', (username,))
rows = cursor.fetchall()
logger.info(f"Found {len(rows)} rows for username '{username}'") # DEBUG
conn.close()
connections = []
for i, row in enumerate(rows):
try:
logger.info(f"Processing row {i}: id={row[0]}, name={row[1]}") # DEBUG
conn_dict = {
'id': row[0],
'name': row[1],
'host': row[2],
'port': row[3],
'username': row[4],
'authType': row[5],
'password': self._decrypt(row[6]) if row[6] else '',
'key': self._decrypt(row[7]) if row[7] else '',
'wsUrl': row[8] if row[8] else 'ws://localhost:2222',
'lockTimeout': row[9] if row[9] is not None else 15,
'disconnectTimeout': row[10] if row[10] is not None else 60,
}
connections.append(conn_dict)
logger.info(f"Successfully processed row {i}") # DEBUG
except Exception as row_error:
logger.error(f"Error processing row {i}: {row_error}")
import traceback
logger.error(traceback.format_exc())
return connections
except Exception as e:
logger.error(f"Failed to get connections: {e}")
import traceback
logger.error(traceback.format_exc())
return []
def delete_connection(self, username: str, conn_id: str) -> bool:
"""Delete a connection."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"DELETE FROM connections WHERE id = ? AND username = ?",
(conn_id, username)
)
conn.commit()
conn.close()
logger.info(f"Deleted connection {conn_id} for user {username}")
return True
except Exception as e:
logger.error(f"Failed to delete connection: {e}")
return False
# Session Recording Methods
def save_recording(self, recording_data: Dict[str, Any]) -> bool:
"""Save recording metadata to database."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO recordings
(id, username, connection_id, connection_name, session_id, host, ssh_username,
started_at, ended_at, duration_seconds, file_size_bytes, file_path, width, height)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
recording_data['id'],
recording_data['username'],
recording_data['connection_id'],
recording_data.get('connection_name', recording_data['host']),
recording_data['session_id'],
recording_data['host'],
recording_data['ssh_username'],
recording_data.get('started_at'),
recording_data.get('ended_at'),
recording_data.get('duration_seconds'),
recording_data.get('file_size_bytes'),
recording_data['file_path'],
recording_data.get('width', 80),
recording_data.get('height', 24)
))
conn.commit()
conn.close()
logger.info(f"Saved recording metadata: {recording_data['id']}")
return True
except Exception as e:
logger.error(f"Failed to save recording: {e}")
return False
def update_recording_end(self, recording_id: str, ended_at: str, duration: int, file_size: int) -> bool:
"""Update recording with end time and file info."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE recordings
SET ended_at = ?, duration_seconds = ?, file_size_bytes = ?
WHERE id = ?
''', (ended_at, duration, file_size, recording_id))
conn.commit()
conn.close()
logger.info(f"Updated recording end: {recording_id}")
return True
except Exception as e:
logger.error(f"Failed to update recording: {e}")
return False
def get_recordings(self, username: str, connection_id: str = None) -> list:
"""Get all recordings for a user, optionally filtered by connection."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if connection_id:
cursor.execute('''
SELECT id, connection_id, connection_name, session_id, host, ssh_username,
started_at, ended_at, duration_seconds, file_size_bytes, file_path, width, height
FROM recordings WHERE username = ? AND connection_id = ?
ORDER BY started_at DESC
''', (username, connection_id))
else:
cursor.execute('''
SELECT id, connection_id, connection_name, session_id, host, ssh_username,
started_at, ended_at, duration_seconds, file_size_bytes, file_path, width, height
FROM recordings WHERE username = ?
ORDER BY started_at DESC
''', (username,))
rows = cursor.fetchall()
conn.close()
recordings = []
for row in rows:
recordings.append({
'id': row[0],
'connection_id': row[1],
'connection_name': row[2],
'session_id': row[3],
'host': row[4],
'ssh_username': row[5],
'started_at': row[6],
'ended_at': row[7],
'duration_seconds': row[8],
'file_size_bytes': row[9],
'file_path': row[10],
'width': row[11],
'height': row[12]
})
return recordings
except Exception as e:
logger.error(f"Failed to get recordings: {e}")
return []
def get_recording_by_id(self, recording_id: str, username: str) -> Optional[Dict]:
"""Get a specific recording by ID."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT id, connection_id, connection_name, session_id, host, ssh_username,
started_at, ended_at, duration_seconds, file_size_bytes, file_path, width, height
FROM recordings WHERE id = ? AND username = ?
''', (recording_id, username))
row = cursor.fetchone()
conn.close()
if row:
return {
'id': row[0],
'connection_id': row[1],
'connection_name': row[2],
'session_id': row[3],
'host': row[4],
'ssh_username': row[5],
'started_at': row[6],
'ended_at': row[7],
'duration_seconds': row[8],
'file_size_bytes': row[9],
'file_path': row[10],
'width': row[11],
'height': row[12]
}
return None
except Exception as e:
logger.error(f"Failed to get recording: {e}")
return None
def delete_recording(self, recording_id: str, username: str) -> bool:
"""Delete a recording from database."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"DELETE FROM recordings WHERE id = ? AND username = ?",
(recording_id, username)
)
conn.commit()
conn.close()
logger.info(f"Deleted recording {recording_id} for user {username}")
return True
except Exception as e:
logger.error(f"Failed to delete recording: {e}")
return False
def get_connections_with_recordings(self, username: str) -> list:
"""Get unique connections that have recordings for a user."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT DISTINCT connection_id, connection_name, host, ssh_username,
COUNT(*) as recording_count,
SUM(file_size_bytes) as total_size,
MAX(started_at) as last_recording
FROM recordings
WHERE username = ? AND connection_id IS NOT NULL
GROUP BY connection_id, connection_name, host, ssh_username
ORDER BY last_recording DESC
''', (username,))
rows = cursor.fetchall()
conn.close()
return [{
'connection_id': row[0],
'connection_name': row[1],
'host': row[2],
'ssh_username': row[3],
'recording_count': row[4],
'total_size_bytes': row[5] or 0,
'last_recording': row[6]
} for row in rows]
except Exception as e:
logger.error(f"Failed to get connections with recordings: {e}")
return []
def delete_recordings_by_connection(self, username: str, connection_id: str) -> tuple:
"""Delete all recordings for a specific connection. Returns (success_count, failed_count)."""
try:
# First get all recordings to delete files
recordings = self.get_recordings(username, connection_id)
success_count = 0
failed_count = 0
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for rec in recordings:
try:
# Delete file if exists
file_path = rec.get('file_path')
if file_path and os.path.exists(file_path):
os.remove(file_path)
# Delete from database
cursor.execute(
"DELETE FROM recordings WHERE id = ? AND username = ?",
(rec['id'], username)
)
success_count += 1
except Exception as e:
logger.error(f"Failed to delete recording {rec.get('id')}: {e}")
failed_count += 1
conn.commit()
conn.close()
logger.info(f"Deleted {success_count} recordings for connection {connection_id}, failed: {failed_count}")
return (success_count, failed_count)
except Exception as e:
logger.error(f"Failed to delete recordings by connection: {e}")
return (0, 0)
def get_old_recordings(self, before_date: str) -> list:
"""Get recordings older than a specific date for cleanup."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT id, username, file_path FROM recordings
WHERE started_at < ?
''', (before_date,))
rows = cursor.fetchall()
conn.close()
return [{'id': row[0], 'username': row[1], 'file_path': row[2]} for row in rows]
except Exception as e:
logger.error(f"Failed to get old recordings: {e}")
return []
# Initialize credentials manager (singleton)
try:
creds_manager = CredentialsManager(STORAGE_FILE, ENCRYPTION_KEY)
logger.info("Credentials manager initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize credentials manager: {e}")
import traceback
logger.error(traceback.format_exc())
creds_manager = None # Will fail later when used