fix: full security audit remediation — P0/P1/P2/P3 fixes + 1020 passing tests
P0 — Broken functionality: - Fix 12+ endpoints with wrong manager method signatures (email/calendar/file/routing) - Fix email_manager.delete_email_user() missing domain arg - Fix cell-link DNS forwarding wiped on every peer change (generate_corefile now accepts cell_links param; add/remove_cell_dns_forward no longer clobber the file) - Fix Flask SECRET_KEY regenerating on every restart (persisted to DATA_DIR) - Fix _next_peer_ip exhaustion returning 500 instead of 409 - Fix ConfigManager Caddyfile path (/app/config-caddy/) - Fix UI double-add and wrong-key peer bugs in Peers.jsx / WireGuard.jsx - Remove hardcoded credentials from Dashboard.jsx P1 — Security: - CSRF token validation on all POST/PUT/DELETE/PATCH to /api/* (double-submit pattern) - enforce_auth: 503 only when users file readable but empty; never bypass on IOError - WireGuard add_cell_peer: validate pubkey, name, endpoint against strict regexes - DNS add_cell_dns_forward: validate IP and domain; reject injection chars - DNS zone write: realpath containment + record content validation - iptables comment /32 suffix prevents substring match deleting wrong peer rules - is_local_request() trusts only loopback + 172.16.0.0/12 (Docker bridge) - POST /api/containers: volume allow-list prevents arbitrary host mounts - file_manager: bcrypt ($2b→$2y) for WebDAV; realpath containment in delete_user - email/calendar: stop persisting plaintext passwords in user records - routing_manager: validate IPs, networks, and interface names - peer_registry: write peers.json at mode 0o600 - vault_manager: Fernet key file at mode 0o600 - CORS: lock down to explicit origin list - domain/cell_name validation: reject newline, brace, semicolon injection chars P2 — Architecture: - Peer add: rollback registry entry if firewall rules fail post-add - restart_service(): base class now calls _restart_container(); email and calendar managers call cell-mail / cell-radicale respectively - email/calendar managers sync user list (no passwords) to cell_config.json - Pending-restart flag cleared only after helper subprocess exits with code 0 - docker-compose.yml: add config-caddy volume to API container P3 — Tests (854 → 1020): - Fill test_email_endpoints.py, test_calendar_endpoints.py, test_network_endpoints.py, test_routing_endpoints.py - New: test_peer_management_update.py, test_peer_management_edge_cases.py, test_input_validation.py, test_enforce_auth_configured.py, test_cell_link_dns.py, test_logs_endpoints.py, test_cells_endpoints.py, test_is_local_request_per_endpoint.py, test_caddy_routing.py - E2E conftest: skip WireGuard suite when wg-quick absent - Update existing tests to match fixed signatures and comment formats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+359
-340
@@ -1,341 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Peer Registry for Personal Internet Cell
|
||||
Handles peer registration and management
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from threading import RLock
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from base_service_manager import BaseServiceManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PeerRegistry(BaseServiceManager):
|
||||
"""Manages peer registration and management"""
|
||||
|
||||
def __init__(self, data_dir: str = '/app/data', config_dir: str = '/app/config'):
|
||||
super().__init__('peer_registry', data_dir, config_dir)
|
||||
self.lock = RLock()
|
||||
self.peers = []
|
||||
self.peers_file = os.path.join(data_dir, 'peers.json')
|
||||
self._load_peers()
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get peer registry status"""
|
||||
try:
|
||||
with self.lock:
|
||||
status = {
|
||||
'running': True,
|
||||
'status': 'online',
|
||||
'peers_count': len(self.peers),
|
||||
'active_peers': len([p for p in self.peers if p.get('active', True)]),
|
||||
'inactive_peers': len([p for p in self.peers if not p.get('active', True)]),
|
||||
'last_updated': datetime.utcnow().isoformat(),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return status
|
||||
except Exception as e:
|
||||
return self.handle_error(e, "get_status")
|
||||
|
||||
def test_connectivity(self) -> Dict[str, Any]:
|
||||
"""Test peer registry connectivity"""
|
||||
try:
|
||||
# Test file system access
|
||||
fs_test = self._test_filesystem_access()
|
||||
|
||||
# Test peer data integrity
|
||||
integrity_test = self._test_data_integrity()
|
||||
|
||||
# Test peer operations
|
||||
operations_test = self._test_peer_operations()
|
||||
|
||||
results = {
|
||||
'filesystem_access': fs_test,
|
||||
'data_integrity': integrity_test,
|
||||
'peer_operations': operations_test,
|
||||
'success': fs_test.get('success', False) and integrity_test.get('success', False),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
return self.handle_error(e, "test_connectivity")
|
||||
|
||||
def _test_filesystem_access(self) -> Dict[str, Any]:
|
||||
"""Test filesystem access for peer data"""
|
||||
try:
|
||||
# Test if we can read/write to the peers file
|
||||
test_peer = {
|
||||
'peer': 'test_peer',
|
||||
'ip': '192.168.1.100',
|
||||
'public_key': 'test_key',
|
||||
'active': False,
|
||||
'test': True
|
||||
}
|
||||
|
||||
# Test write
|
||||
with self.lock:
|
||||
original_peers = self.peers.copy()
|
||||
self.peers.append(test_peer)
|
||||
self._save_peers()
|
||||
|
||||
# Test read
|
||||
with self.lock:
|
||||
loaded_peers = self.peers.copy()
|
||||
# Remove test peer
|
||||
self.peers = [p for p in self.peers if not p.get('test', False)]
|
||||
self._save_peers()
|
||||
|
||||
# Restore original state
|
||||
with self.lock:
|
||||
self.peers = original_peers
|
||||
self._save_peers()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Filesystem access working',
|
||||
'read_write': True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Filesystem access failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _test_data_integrity(self) -> Dict[str, Any]:
|
||||
"""Test peer data integrity"""
|
||||
try:
|
||||
with self.lock:
|
||||
# Check if peers data is valid JSON
|
||||
peers_copy = self.peers.copy()
|
||||
|
||||
# Validate peer structure
|
||||
valid_peers = 0
|
||||
invalid_peers = 0
|
||||
|
||||
for peer in peers_copy:
|
||||
if isinstance(peer, dict) and 'peer' in peer and 'ip' in peer:
|
||||
valid_peers += 1
|
||||
else:
|
||||
invalid_peers += 1
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Data integrity check passed',
|
||||
'valid_peers': valid_peers,
|
||||
'invalid_peers': invalid_peers,
|
||||
'total_peers': len(peers_copy)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Data integrity check failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _test_peer_operations(self) -> Dict[str, Any]:
|
||||
"""Test peer operations"""
|
||||
try:
|
||||
# Test adding a peer
|
||||
test_peer = {
|
||||
'peer': 'test_operation_peer',
|
||||
'ip': '192.168.1.101',
|
||||
'public_key': 'test_operation_key',
|
||||
'active': False,
|
||||
'test': True
|
||||
}
|
||||
|
||||
# Test add
|
||||
add_success = self.add_peer(test_peer)
|
||||
|
||||
# Test get
|
||||
retrieved_peer = self.get_peer('test_operation_peer')
|
||||
get_success = retrieved_peer is not None
|
||||
|
||||
# Test update
|
||||
update_success = self.update_peer_ip('test_operation_peer', '192.168.1.102')
|
||||
|
||||
# Test remove
|
||||
remove_success = self.remove_peer('test_operation_peer')
|
||||
|
||||
return {
|
||||
'success': add_success and get_success and update_success and remove_success,
|
||||
'message': 'Peer operations working',
|
||||
'add_success': add_success,
|
||||
'get_success': get_success,
|
||||
'update_success': update_success,
|
||||
'remove_success': remove_success
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Peer operations test failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _load_peers(self):
|
||||
"""Load peers from file"""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self.peers_file), exist_ok=True)
|
||||
|
||||
if os.path.exists(self.peers_file):
|
||||
with open(self.peers_file, 'r') as f:
|
||||
try:
|
||||
self.peers = json.load(f)
|
||||
self.logger.info(f"Loaded {len(self.peers)} peers from file")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading peers: {e}")
|
||||
self.peers = []
|
||||
else:
|
||||
self.peers = []
|
||||
self.logger.info("No peers file found, starting with empty registry")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in _load_peers: {e}")
|
||||
self.peers = []
|
||||
|
||||
def _save_peers(self):
|
||||
"""Save peers to file"""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self.peers_file), exist_ok=True)
|
||||
|
||||
with open(self.peers_file, 'w') as f:
|
||||
json.dump(self.peers, f, indent=2)
|
||||
|
||||
self.logger.info(f"Saved {len(self.peers)} peers to file")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error saving peers: {e}")
|
||||
|
||||
def list_peers(self) -> List[Dict[str, Any]]:
|
||||
"""List all peers"""
|
||||
with self.lock:
|
||||
return list(self.peers)
|
||||
|
||||
def get_peer(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a specific peer by name"""
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
return peer
|
||||
return None
|
||||
|
||||
def add_peer(self, peer_info: Dict[str, Any]) -> bool:
|
||||
"""Add a new peer"""
|
||||
try:
|
||||
with self.lock:
|
||||
if self.get_peer(peer_info.get('peer')):
|
||||
self.logger.warning(f"Peer {peer_info.get('peer')} already exists")
|
||||
return False
|
||||
|
||||
# Add timestamp
|
||||
peer_info['created_at'] = datetime.utcnow().isoformat()
|
||||
peer_info['active'] = peer_info.get('active', True)
|
||||
|
||||
self.peers.append(peer_info)
|
||||
self._save_peers()
|
||||
|
||||
self.logger.info(f"Added peer: {peer_info.get('peer')}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error adding peer: {e}")
|
||||
return False
|
||||
|
||||
def remove_peer(self, name: str) -> bool:
|
||||
"""Remove a peer"""
|
||||
try:
|
||||
with self.lock:
|
||||
before = len(self.peers)
|
||||
self.peers = [p for p in self.peers if p.get('peer') != name]
|
||||
self._save_peers()
|
||||
|
||||
removed = len(self.peers) < before
|
||||
if removed:
|
||||
self.logger.info(f"Removed peer: {name}")
|
||||
else:
|
||||
self.logger.warning(f"Peer {name} not found for removal")
|
||||
|
||||
return removed
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error removing peer {name}: {e}")
|
||||
return False
|
||||
|
||||
def update_peer(self, name: str, fields: Dict[str, Any]) -> bool:
|
||||
"""Update arbitrary fields on a peer."""
|
||||
try:
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
peer.update(fields)
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
self.logger.info(f"Updated peer {name}: {list(fields.keys())}")
|
||||
return True
|
||||
self.logger.warning(f"Peer {name} not found for update")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating peer {name}: {e}")
|
||||
return False
|
||||
|
||||
def clear_reinstall_flag(self, name: str) -> bool:
|
||||
"""Clear the config_needs_reinstall flag after user downloads new config."""
|
||||
return self.update_peer(name, {'config_needs_reinstall': False})
|
||||
|
||||
def update_peer_ip(self, name: str, new_ip: str) -> bool:
|
||||
"""Update peer IP address"""
|
||||
try:
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
old_ip = peer.get('ip')
|
||||
peer['ip'] = new_ip
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
|
||||
self.logger.info(f"Updated peer {name} IP from {old_ip} to {new_ip}")
|
||||
return True
|
||||
|
||||
self.logger.warning(f"Peer {name} not found for IP update")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating peer {name} IP: {e}")
|
||||
return False
|
||||
|
||||
def get_peer_stats(self) -> Dict[str, Any]:
|
||||
"""Get peer registry statistics"""
|
||||
try:
|
||||
with self.lock:
|
||||
active_peers = [p for p in self.peers if p.get('active', True)]
|
||||
inactive_peers = [p for p in self.peers if not p.get('active', True)]
|
||||
|
||||
# Count peers by IP range
|
||||
ip_ranges = {}
|
||||
for peer in self.peers:
|
||||
ip = peer.get('ip', '')
|
||||
if ip:
|
||||
range_key = '.'.join(ip.split('.')[:3]) + '.0/24'
|
||||
ip_ranges[range_key] = ip_ranges.get(range_key, 0) + 1
|
||||
|
||||
return {
|
||||
'total_peers': len(self.peers),
|
||||
'active_peers': len(active_peers),
|
||||
'inactive_peers': len(inactive_peers),
|
||||
'ip_ranges': ip_ranges,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting peer stats: {e}")
|
||||
return {
|
||||
'total_peers': 0,
|
||||
'active_peers': 0,
|
||||
'inactive_peers': 0,
|
||||
'ip_ranges': {},
|
||||
'error': str(e),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Peer Registry for Personal Internet Cell
|
||||
Handles peer registration and management
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from threading import RLock
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from base_service_manager import BaseServiceManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PeerRegistry(BaseServiceManager):
|
||||
"""Manages peer registration and management"""
|
||||
|
||||
def __init__(self, data_dir: str = '/app/data', config_dir: str = '/app/config'):
|
||||
super().__init__('peer_registry', data_dir, config_dir)
|
||||
self.lock = RLock()
|
||||
self.peers = []
|
||||
self.peers_file = os.path.join(data_dir, 'peers.json')
|
||||
self._load_peers()
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get peer registry status"""
|
||||
try:
|
||||
with self.lock:
|
||||
status = {
|
||||
'running': True,
|
||||
'status': 'online',
|
||||
'peers_count': len(self.peers),
|
||||
'active_peers': len([p for p in self.peers if p.get('active', True)]),
|
||||
'inactive_peers': len([p for p in self.peers if not p.get('active', True)]),
|
||||
'last_updated': datetime.utcnow().isoformat(),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return status
|
||||
except Exception as e:
|
||||
return self.handle_error(e, "get_status")
|
||||
|
||||
def test_connectivity(self) -> Dict[str, Any]:
|
||||
"""Test peer registry connectivity"""
|
||||
try:
|
||||
# Test file system access
|
||||
fs_test = self._test_filesystem_access()
|
||||
|
||||
# Test peer data integrity
|
||||
integrity_test = self._test_data_integrity()
|
||||
|
||||
# Test peer operations
|
||||
operations_test = self._test_peer_operations()
|
||||
|
||||
results = {
|
||||
'filesystem_access': fs_test,
|
||||
'data_integrity': integrity_test,
|
||||
'peer_operations': operations_test,
|
||||
'success': fs_test.get('success', False) and integrity_test.get('success', False),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
return self.handle_error(e, "test_connectivity")
|
||||
|
||||
def _test_filesystem_access(self) -> Dict[str, Any]:
|
||||
"""Test filesystem access for peer data"""
|
||||
try:
|
||||
# Test if we can read/write to the peers file
|
||||
test_peer = {
|
||||
'peer': 'test_peer',
|
||||
'ip': '192.168.1.100',
|
||||
'public_key': 'test_key',
|
||||
'active': False,
|
||||
'test': True
|
||||
}
|
||||
|
||||
# Test write
|
||||
with self.lock:
|
||||
original_peers = self.peers.copy()
|
||||
self.peers.append(test_peer)
|
||||
self._save_peers()
|
||||
|
||||
# Test read
|
||||
with self.lock:
|
||||
loaded_peers = self.peers.copy()
|
||||
# Remove test peer
|
||||
self.peers = [p for p in self.peers if not p.get('test', False)]
|
||||
self._save_peers()
|
||||
|
||||
# Restore original state
|
||||
with self.lock:
|
||||
self.peers = original_peers
|
||||
self._save_peers()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Filesystem access working',
|
||||
'read_write': True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Filesystem access failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _test_data_integrity(self) -> Dict[str, Any]:
|
||||
"""Test peer data integrity"""
|
||||
try:
|
||||
with self.lock:
|
||||
# Check if peers data is valid JSON
|
||||
peers_copy = self.peers.copy()
|
||||
|
||||
# Validate peer structure
|
||||
valid_peers = 0
|
||||
invalid_peers = 0
|
||||
|
||||
for peer in peers_copy:
|
||||
if isinstance(peer, dict) and 'peer' in peer and 'ip' in peer:
|
||||
valid_peers += 1
|
||||
else:
|
||||
invalid_peers += 1
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Data integrity check passed',
|
||||
'valid_peers': valid_peers,
|
||||
'invalid_peers': invalid_peers,
|
||||
'total_peers': len(peers_copy)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Data integrity check failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _test_peer_operations(self) -> Dict[str, Any]:
|
||||
"""Test peer operations"""
|
||||
try:
|
||||
# Test adding a peer
|
||||
test_peer = {
|
||||
'peer': 'test_operation_peer',
|
||||
'ip': '192.168.1.101',
|
||||
'public_key': 'test_operation_key',
|
||||
'active': False,
|
||||
'test': True
|
||||
}
|
||||
|
||||
# Test add
|
||||
add_success = self.add_peer(test_peer)
|
||||
|
||||
# Test get
|
||||
retrieved_peer = self.get_peer('test_operation_peer')
|
||||
get_success = retrieved_peer is not None
|
||||
|
||||
# Test update
|
||||
update_success = self.update_peer_ip('test_operation_peer', '192.168.1.102')
|
||||
|
||||
# Test remove
|
||||
remove_success = self.remove_peer('test_operation_peer')
|
||||
|
||||
return {
|
||||
'success': add_success and get_success and update_success and remove_success,
|
||||
'message': 'Peer operations working',
|
||||
'add_success': add_success,
|
||||
'get_success': get_success,
|
||||
'update_success': update_success,
|
||||
'remove_success': remove_success
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Peer operations test failed: {str(e)}',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _load_peers(self):
|
||||
"""Load peers from file"""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self.peers_file), exist_ok=True)
|
||||
|
||||
if os.path.exists(self.peers_file):
|
||||
with open(self.peers_file, 'r') as f:
|
||||
try:
|
||||
self.peers = json.load(f)
|
||||
self.logger.info(f"Loaded {len(self.peers)} peers from file")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading peers: {e}")
|
||||
self.peers = []
|
||||
else:
|
||||
self.peers = []
|
||||
self.logger.info("No peers file found, starting with empty registry")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in _load_peers: {e}")
|
||||
self.peers = []
|
||||
|
||||
def _save_peers(self):
|
||||
"""Save peers to file"""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(self.peers_file), exist_ok=True)
|
||||
|
||||
# Write to a temp file with restrictive perms, then atomically replace.
|
||||
# peers.json contains WireGuard private keys — must never be world-readable.
|
||||
tmp_path = self.peers_file + '.tmp'
|
||||
# Open with O_CREAT|O_WRONLY|O_TRUNC and mode 0o600 so the file is
|
||||
# created with restrictive permissions from the very first byte.
|
||||
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
json.dump(self.peers, f, indent=2)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
# Ensure perms are 0o600 even if umask or prior file affected them.
|
||||
os.chmod(tmp_path, 0o600)
|
||||
os.replace(tmp_path, self.peers_file)
|
||||
# Belt-and-braces: also chmod the destination in case it pre-existed
|
||||
# with looser perms on a filesystem that preserves the destination's mode.
|
||||
os.chmod(self.peers_file, 0o600)
|
||||
|
||||
self.logger.info(f"Saved {len(self.peers)} peers to file")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error saving peers: {e}")
|
||||
|
||||
def list_peers(self) -> List[Dict[str, Any]]:
|
||||
"""List all peers"""
|
||||
with self.lock:
|
||||
return list(self.peers)
|
||||
|
||||
def get_peer(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a specific peer by name"""
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
return peer
|
||||
return None
|
||||
|
||||
def add_peer(self, peer_info: Dict[str, Any]) -> bool:
|
||||
"""Add a new peer"""
|
||||
try:
|
||||
with self.lock:
|
||||
if self.get_peer(peer_info.get('peer')):
|
||||
self.logger.warning(f"Peer {peer_info.get('peer')} already exists")
|
||||
return False
|
||||
|
||||
# Add timestamp
|
||||
peer_info['created_at'] = datetime.utcnow().isoformat()
|
||||
peer_info['active'] = peer_info.get('active', True)
|
||||
|
||||
self.peers.append(peer_info)
|
||||
self._save_peers()
|
||||
|
||||
self.logger.info(f"Added peer: {peer_info.get('peer')}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error adding peer: {e}")
|
||||
return False
|
||||
|
||||
def remove_peer(self, name: str) -> bool:
|
||||
"""Remove a peer"""
|
||||
try:
|
||||
with self.lock:
|
||||
before = len(self.peers)
|
||||
self.peers = [p for p in self.peers if p.get('peer') != name]
|
||||
self._save_peers()
|
||||
|
||||
removed = len(self.peers) < before
|
||||
if removed:
|
||||
self.logger.info(f"Removed peer: {name}")
|
||||
else:
|
||||
self.logger.warning(f"Peer {name} not found for removal")
|
||||
|
||||
return removed
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error removing peer {name}: {e}")
|
||||
return False
|
||||
|
||||
def update_peer(self, name: str, fields: Dict[str, Any]) -> bool:
|
||||
"""Update arbitrary fields on a peer."""
|
||||
try:
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
peer.update(fields)
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
self.logger.info(f"Updated peer {name}: {list(fields.keys())}")
|
||||
return True
|
||||
self.logger.warning(f"Peer {name} not found for update")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating peer {name}: {e}")
|
||||
return False
|
||||
|
||||
def clear_reinstall_flag(self, name: str) -> bool:
|
||||
"""Clear the config_needs_reinstall flag after user downloads new config."""
|
||||
return self.update_peer(name, {'config_needs_reinstall': False})
|
||||
|
||||
def update_peer_ip(self, name: str, new_ip: str) -> bool:
|
||||
"""Update peer IP address"""
|
||||
try:
|
||||
with self.lock:
|
||||
for peer in self.peers:
|
||||
if peer.get('peer') == name:
|
||||
old_ip = peer.get('ip')
|
||||
peer['ip'] = new_ip
|
||||
peer['updated_at'] = datetime.utcnow().isoformat()
|
||||
self._save_peers()
|
||||
|
||||
self.logger.info(f"Updated peer {name} IP from {old_ip} to {new_ip}")
|
||||
return True
|
||||
|
||||
self.logger.warning(f"Peer {name} not found for IP update")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating peer {name} IP: {e}")
|
||||
return False
|
||||
|
||||
def get_peer_stats(self) -> Dict[str, Any]:
|
||||
"""Get peer registry statistics"""
|
||||
try:
|
||||
with self.lock:
|
||||
active_peers = [p for p in self.peers if p.get('active', True)]
|
||||
inactive_peers = [p for p in self.peers if not p.get('active', True)]
|
||||
|
||||
# Count peers by IP range
|
||||
ip_ranges = {}
|
||||
for peer in self.peers:
|
||||
ip = peer.get('ip', '')
|
||||
if ip:
|
||||
range_key = '.'.join(ip.split('.')[:3]) + '.0/24'
|
||||
ip_ranges[range_key] = ip_ranges.get(range_key, 0) + 1
|
||||
|
||||
return {
|
||||
'total_peers': len(self.peers),
|
||||
'active_peers': len(active_peers),
|
||||
'inactive_peers': len(inactive_peers),
|
||||
'ip_ranges': ip_ranges,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting peer stats: {e}")
|
||||
return {
|
||||
'total_peers': 0,
|
||||
'active_peers': 0,
|
||||
'inactive_peers': 0,
|
||||
'ip_ranges': {},
|
||||
'error': str(e),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
Reference in New Issue
Block a user