mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 15:44:58 +00:00
805 lines
31 KiB
Python
805 lines
31 KiB
Python
# shared.py
|
||
# Core component for managing shared resources and data for Bjorn project
|
||
# Handles initialization, configuration, logging, fonts, images, and database management
|
||
|
||
import os
|
||
import re
|
||
import importlib
|
||
import random
|
||
import time
|
||
import ast
|
||
import logging
|
||
import subprocess
|
||
import threading
|
||
from typing import Dict, List, Optional, Any
|
||
from PIL import Image, ImageFont
|
||
from logger import Logger
|
||
# from epd_helper import EPDHelper
|
||
from epd_manager import EPDManager
|
||
|
||
from database import BjornDatabase
|
||
|
||
logger = Logger(name="shared.py", level=logging.DEBUG)
|
||
|
||
|
||
class SharedData:
|
||
"""Centralized shared data manager for all Bjorn modules"""
|
||
|
||
def __init__(self):
|
||
# Initialize core paths first
|
||
self.initialize_paths()
|
||
|
||
# Initialize status tracking
|
||
self.status_list = []
|
||
self.last_comment_time = time.time()
|
||
|
||
# Event for orchestrator wake-up (Avoids CPU busy-waiting)
|
||
self.queue_event = threading.Event()
|
||
|
||
# Load default configuration
|
||
self.default_config = self.get_default_config()
|
||
self.config = self.default_config.copy()
|
||
|
||
# Initialize database (single source of truth)
|
||
self.db = BjornDatabase()
|
||
|
||
# Load existing configuration from database
|
||
self.load_config()
|
||
|
||
# Update security blacklists
|
||
self.update_security_blacklists()
|
||
|
||
# Setup environment and resources
|
||
self.setup_environment()
|
||
self.initialize_runtime_variables()
|
||
self.initialize_statistics()
|
||
self.load_fonts()
|
||
self.load_images()
|
||
|
||
logger.info("SharedData initialization complete")
|
||
|
||
def initialize_paths(self):
|
||
"""Initialize all application paths and create necessary directories"""
|
||
# Base directories
|
||
self.bjorn_user_dir = '/home/bjorn/'
|
||
self.current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# Main application directories
|
||
self.data_dir = os.path.join(self.current_dir, 'data')
|
||
self.actions_dir = os.path.join(self.current_dir, 'actions')
|
||
self.web_dir = os.path.join(self.current_dir, 'web')
|
||
self.resources_dir = os.path.join(self.current_dir, 'resources')
|
||
|
||
# User directories
|
||
self.backup_dir = '/home/bjorn/.backups_bjorn'
|
||
self.settings_dir = '/home/bjorn/.settings_bjorn'
|
||
|
||
# Data subdirectories
|
||
self.logs_dir = os.path.join(self.data_dir, 'logs')
|
||
self.output_dir = os.path.join(self.data_dir, 'output')
|
||
self.input_dir = os.path.join(self.data_dir, 'input')
|
||
|
||
|
||
# Output subdirectories
|
||
self.data_stolen_dir = os.path.join(self.output_dir, 'data_stolen')
|
||
|
||
# Resources subdirectories
|
||
self.images_dir = os.path.join(self.resources_dir, 'images')
|
||
self.fonts_dir = os.path.join(self.resources_dir, 'fonts')
|
||
self.default_config_dir = os.path.join(self.resources_dir, 'default_config')
|
||
self.default_comments_dir = os.path.join(self.default_config_dir, 'comments')
|
||
|
||
# Default config subdirectories
|
||
self.default_comments_file = os.path.join(self.default_comments_dir, 'comments.en.json')
|
||
self.default_images_dir = os.path.join(self.default_config_dir, 'images')
|
||
self.default_actions_dir = os.path.join(self.default_config_dir, 'actions')
|
||
|
||
# Images subdirectories
|
||
self.status_images_dir = os.path.join(self.images_dir, 'status')
|
||
self.static_images_dir = os.path.join(self.images_dir, 'static')
|
||
|
||
# Input subdirectories
|
||
self.dictionary_dir = os.path.join(self.input_dir, "dictionary")
|
||
self.potfiles_dir = os.path.join(self.input_dir, "potfiles")
|
||
self.wordlists_dir = os.path.join(self.input_dir, "wordlists")
|
||
self.nmap_prefixes_dir = os.path.join(self.input_dir, "prefixes")
|
||
|
||
# Actions subdirectory
|
||
self.actions_icons_dir = os.path.join(self.actions_dir, 'actions_icons')
|
||
|
||
# Important files
|
||
self.version_file = os.path.join(self.current_dir, 'version.txt')
|
||
self.backups_json = os.path.join(self.backup_dir, 'backups.json')
|
||
self.webapp_json = os.path.join(self.settings_dir, 'webapp.json')
|
||
self.nmap_prefixes_file = os.path.join(self.nmap_prefixes_dir, "nmap-mac-prefixes.txt")
|
||
self.common_wordlist = os.path.join(self.wordlists_dir, "common.txt")
|
||
self.users_file = os.path.join(self.dictionary_dir, "users.txt")
|
||
self.passwords_file = os.path.join(self.dictionary_dir, "passwords.txt")
|
||
self.log_file = os.path.join(self.logs_dir, 'Bjorn.log')
|
||
self.web_console_log = os.path.join(self.logs_dir, 'web_console_log.txt')
|
||
|
||
# Create all necessary directories
|
||
self._create_directories()
|
||
|
||
def _create_directories(self):
|
||
"""Create all necessary directories if they don't exist"""
|
||
directories = [
|
||
self.data_dir, self.actions_dir, self.web_dir, self.resources_dir,
|
||
self.logs_dir, self.output_dir, self.input_dir,
|
||
self.data_stolen_dir, self.images_dir, self.fonts_dir,
|
||
self.fonts_dir, self.default_config_dir, self.default_comments_dir,
|
||
self.status_images_dir, self.static_images_dir, self.dictionary_dir,
|
||
self.potfiles_dir, self.wordlists_dir, self.nmap_prefixes_dir,
|
||
self.backup_dir, self.settings_dir
|
||
]
|
||
|
||
for directory in directories:
|
||
try:
|
||
os.makedirs(directory, exist_ok=True)
|
||
except Exception as e:
|
||
logger.error(f"Cannot create directory {directory}: {e}")
|
||
|
||
def get_default_config(self) -> Dict[str, Any]:
|
||
"""Return default configuration settings"""
|
||
return {
|
||
# General Settings
|
||
"__title_Bjorn__": "Settings",
|
||
"bjorn_name": "Bjorn",
|
||
"current_character": "BJORN",
|
||
"manual_mode": False,
|
||
"debug_mode": True,
|
||
"lang_priority":["en", "fr", "es"] ,
|
||
"lang": "en",
|
||
|
||
# Web Server Settings
|
||
"websrv": True,
|
||
"webauth": False,
|
||
"retry_success_actions": False,
|
||
"retry_failed_actions": True,
|
||
"blacklistcheck": True,
|
||
"consoleonwebstart": True,
|
||
|
||
# Timing Settings
|
||
"startup_delay": 5,
|
||
"web_delay": 2,
|
||
"screen_delay": 1,
|
||
"comment_delaymin": 15,
|
||
"comment_delaymax": 30,
|
||
"livestatus_delay": 8,
|
||
|
||
# Display Settings
|
||
"epd_enabled": True,
|
||
"screen_reversed": True,
|
||
"web_screen_reversed": True,
|
||
"showstartupipssid": False,
|
||
"showiponscreen": True,
|
||
"showssidonscreen": True,
|
||
"shared_update_interval": 10,
|
||
"vuln_update_interval": 20,
|
||
"semaphore_slots": 5,
|
||
"double_partial_refresh": True,
|
||
"startup_splash_duration": 3,
|
||
"fullrefresh_activated": True,
|
||
"fullrefresh_delay": 600,
|
||
"image_display_delaymin": 2,
|
||
"image_display_delaymax": 8,
|
||
|
||
# EPD Display Settings
|
||
"ref_width": 122,
|
||
"ref_height": 250,
|
||
"epd_type": "epd2in13_V4",
|
||
"defaultfonttitle": "Viking.TTF",
|
||
"defaultfont": "Arial.ttf",
|
||
"line_spacing": 1,
|
||
|
||
# Display Positions
|
||
"frise_default_x": 0,
|
||
"frise_default_y": 160,
|
||
"frise_epd2in7_x": 50,
|
||
"frise_epd2in7_y": 160,
|
||
|
||
# Network Interface Settings
|
||
"ip_iface_priority": ["wlan0", "eth0"],
|
||
"neigh_wifi_iface": "wlan0",
|
||
"neigh_ethernet_iface": "eth0",
|
||
"neigh_usb_iface": "usb0",
|
||
"neigh_bluetooth_ifaces": ["pan0", "bnep0"],
|
||
|
||
# Security Lists
|
||
"__title_lists__": "List Settings",
|
||
"portlist": [20, 21, 22, 23, 25, 53, 69, 80, 110, 111, 135, 137, 139, 143,
|
||
161, 162, 389, 443, 445, 512, 513, 514, 587, 636, 993, 995,
|
||
1080, 1433, 1521, 2049, 3306, 3389, 5000, 5001, 5432, 5900,
|
||
8080, 8443, 9090, 10000],
|
||
"mac_scan_blacklist": [],
|
||
"ip_scan_blacklist": [],
|
||
"hostname_scan_blacklist": ["bjorn.home"],
|
||
"steal_file_names": ["ssh.csv", "hack.txt"],
|
||
"steal_file_extensions": [".bjorn", ".hack", ".flag"],
|
||
"ignored_smb_shares": ["print$", "ADMIN$", "IPC$"],
|
||
|
||
# Network Scanning Settings
|
||
"__title_network__": "Network",
|
||
"nmap_scan_aggressivity": "-T2",
|
||
"portstart": 1,
|
||
"portend": 2,
|
||
"use_custom_network": False,
|
||
"custom_network": "192.168.1.0/24",
|
||
"default_network_interface": "wlan0",
|
||
|
||
# Vulnerability Scanning Settings
|
||
"vuln_fast": True,
|
||
"nse_vulners": True,
|
||
"vuln_max_ports": 25,
|
||
"vuln_rescan_on_change_only": False, # (facultatif: force un rescan)
|
||
"vuln_rescan_ttl_seconds": 0,
|
||
"scan_cpe": True,
|
||
"nvd_api_key": "",
|
||
"exploitdb_repo_dir": "/home/bjorn/exploitdb",
|
||
"exploitdb_enabled": True,
|
||
"searchsploit_path": "/home/bjorn/exploitdb/searchsploit",
|
||
"exploitdb_root": "/home/bjorn/exploitdb", # si cloné
|
||
"kev_feed_url": "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
|
||
"epss_api": "https://api.first.org/data/v1/epss?cve=",
|
||
|
||
#Actions Studio Settings
|
||
"__title_actions_studio__": "Actions Studio",
|
||
"use_actions_studio": True,
|
||
|
||
|
||
# Action Timing Settings
|
||
"__title_timewaits__": "Time Wait Settings",
|
||
"timewait_smb": 0,
|
||
"timewait_ssh": 0,
|
||
"timewait_telnet": 0,
|
||
"timewait_ftp": 0,
|
||
"timewait_sql": 0,
|
||
}
|
||
|
||
def get_actions_config(self) -> List[Dict[str, Any]]:
|
||
"""Return actions configuration from database"""
|
||
try:
|
||
return self.db.list_actions()
|
||
except Exception as e:
|
||
logger.error(f"Failed to get actions config from DB: {e}")
|
||
return []
|
||
|
||
def update_security_blacklists(self):
|
||
"""Update MAC and hostname blacklists for security"""
|
||
# Get local MAC address
|
||
mac_address = self.get_raspberry_mac()
|
||
if mac_address:
|
||
self._add_to_blacklist('mac_scan_blacklist', mac_address, 'MAC address')
|
||
else:
|
||
logger.warning("Could not add local MAC to blacklist: MAC address not found")
|
||
|
||
# Add local hostname to blacklist
|
||
bjorn_hostname = "bjorn.home"
|
||
self._add_to_blacklist('hostname_scan_blacklist', bjorn_hostname, 'hostname')
|
||
|
||
def _add_to_blacklist(self, blacklist_key: str, value: str, value_type: str):
|
||
"""Add value to specified blacklist"""
|
||
if blacklist_key not in self.config:
|
||
self.config[blacklist_key] = []
|
||
|
||
if value not in self.config[blacklist_key]:
|
||
self.config[blacklist_key].append(value)
|
||
logger.info(f"Added {value_type} {value} to blacklist")
|
||
else:
|
||
logger.info(f"{value_type} {value} already in blacklist")
|
||
|
||
def get_raspberry_mac(self) -> Optional[str]:
|
||
"""Get MAC address of primary network interface"""
|
||
try:
|
||
# Try wireless interface first
|
||
result = subprocess.run(['cat', '/sys/class/net/wlan0/address'],
|
||
capture_output=True, text=True)
|
||
if result.returncode == 0 and result.stdout.strip():
|
||
return result.stdout.strip().lower()
|
||
|
||
# Fallback to ethernet interface
|
||
result = subprocess.run(['cat', '/sys/class/net/eth0/address'],
|
||
capture_output=True, text=True)
|
||
if result.returncode == 0 and result.stdout.strip():
|
||
return result.stdout.strip().lower()
|
||
|
||
logger.warning("Could not find MAC address for wlan0 or eth0")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting Raspberry Pi MAC address: {e}")
|
||
return None
|
||
|
||
def setup_environment(self):
|
||
"""Setup application environment"""
|
||
os.system('cls' if os.name == 'nt' else 'clear')
|
||
self.save_config()
|
||
self.sync_actions_to_database()
|
||
self.delete_web_console_log()
|
||
self.initialize_database()
|
||
self.initialize_epd_display()
|
||
|
||
def initialize_epd_display(self):
|
||
"""Initialize e-paper display"""
|
||
try:
|
||
logger.info("Initializing EPD display...")
|
||
time.sleep(1)
|
||
|
||
# Utiliser le manager au lieu de l’ancien helper
|
||
self.epd = EPDManager(self.config["epd_type"])
|
||
|
||
# Config orientation
|
||
epd_configs = {
|
||
"epd2in7": (False, False),
|
||
"epd2in13_V2": (True, True),
|
||
"epd2in13_V3": (True, True),
|
||
"epd2in13_V4": (True, True)
|
||
}
|
||
if self.config["epd_type"] in epd_configs:
|
||
self.screen_reversed, self.web_screen_reversed = epd_configs[self.config["epd_type"]]
|
||
logger.info(f"EPD type: {self.config['epd_type']} - reversed: {self.screen_reversed}")
|
||
|
||
# Init hardware une fois
|
||
self.epd.init_full_update()
|
||
self.width, self.height = self.epd.epd.width, self.epd.epd.height
|
||
|
||
# Scaling
|
||
self.ref_width = self.config.get('ref_width', 122)
|
||
self.ref_height = self.config.get('ref_height', 250)
|
||
self.scale_factor_x = self.width / self.ref_width
|
||
self.scale_factor_y = self.height / self.ref_height
|
||
|
||
logger.info(f"EPD {self.config['epd_type']} initialized: {self.width}x{self.height}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error initializing EPD display: {e}")
|
||
raise
|
||
|
||
|
||
def initialize_runtime_variables(self):
|
||
"""Initialize runtime variables"""
|
||
# System state flags
|
||
self.should_exit = False
|
||
self.display_should_exit = False
|
||
self.orchestrator_should_exit = False
|
||
self.webapp_should_exit = False
|
||
|
||
# Instance tracking
|
||
self.bjorn_instance = None
|
||
|
||
# Network state
|
||
self.wifi_connected = False
|
||
self.wifi_changed = False
|
||
self.bluetooth_active = False
|
||
self.ethernet_active = False
|
||
self.pan_connected = False
|
||
self.usb_active = False
|
||
|
||
# Display state
|
||
self.bjorn_character = None
|
||
self.current_path = []
|
||
self.comment_params = {}
|
||
self.bjorn_says = "Hacking away..."
|
||
self.bjorn_orch_status = "IDLE"
|
||
self.bjorn_status_text = "IDLE"
|
||
self.bjorn_status_text2 = "Awakening..."
|
||
self.bjorn_progress = ""
|
||
|
||
# UI positioning
|
||
self.text_frame_top = int(88 * self.scale_factor_x)
|
||
self.text_frame_bottom = int(159 * self.scale_factor_y)
|
||
self.y_text = self.text_frame_top + 2
|
||
|
||
# Statistics
|
||
self.battery_status = 26
|
||
self.target_count = 0
|
||
self.port_count = 0
|
||
self.vuln_count = 0
|
||
self.cred_count = 0
|
||
self.data_count = 0
|
||
self.zombie_count = 0
|
||
self.coin_count = 0
|
||
self.level_count = 0
|
||
self.network_kb_count = 0
|
||
self.attacks_count = 0
|
||
|
||
# Display control
|
||
self.show_first_image = True
|
||
|
||
# Threading
|
||
self.scripts_lock = threading.Lock()
|
||
self.running_scripts = {}
|
||
self.output_lock = threading.Lock()
|
||
|
||
# URLs
|
||
self.github_version_url = "https://raw.githubusercontent.com/infinition/Bjorn/main/version.txt"
|
||
|
||
def initialize_statistics(self):
|
||
"""Initialize statistics in database"""
|
||
try:
|
||
self.db.ensure_stats_initialized()
|
||
self.db.update_livestats(
|
||
total_open_ports=0,
|
||
alive_hosts_count=0,
|
||
all_known_hosts_count=0,
|
||
vulnerabilities_count=0
|
||
)
|
||
logger.info("Statistics initialized in database")
|
||
except Exception as e:
|
||
logger.error(f"Error initializing statistics: {e}")
|
||
|
||
def delete_web_console_log(self):
|
||
"""Delete and recreate web console log file"""
|
||
try:
|
||
if os.path.exists(self.web_console_log):
|
||
os.remove(self.web_console_log)
|
||
logger.info(f"Deleted web console log: {self.web_console_log}")
|
||
|
||
# Recreate empty file
|
||
open(self.web_console_log, 'a').close()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error managing web console log: {e}")
|
||
|
||
def sync_actions_to_database(self):
|
||
"""Sync action definitions from files to database (and keep actions_studio in sync non-destructively)."""
|
||
actions_config = []
|
||
|
||
try:
|
||
for filename in os.listdir(self.actions_dir):
|
||
if not filename.endswith(".py") or filename == "__init__.py":
|
||
continue
|
||
|
||
meta = self._extract_action_metadata(os.path.join(self.actions_dir, filename))
|
||
if not meta:
|
||
continue
|
||
|
||
# Defaults
|
||
meta.setdefault("b_action", "normal")
|
||
meta.setdefault("b_priority", 50)
|
||
meta.setdefault("b_timeout", 300)
|
||
meta.setdefault("b_max_retries", 3)
|
||
meta.setdefault("b_cooldown", 0)
|
||
meta.setdefault("b_stealth_level", 5)
|
||
meta.setdefault("b_risk_level", "medium")
|
||
meta.setdefault("b_enabled", 1)
|
||
|
||
actions_config.append(meta)
|
||
|
||
# Status tracking
|
||
if meta["b_class"] not in self.status_list:
|
||
self.status_list.append(meta["b_class"])
|
||
|
||
if actions_config:
|
||
self.db.sync_actions(actions_config)
|
||
logger.info(f"Synchronized {len(actions_config)} actions to database")
|
||
|
||
# Garde actions_studio alignée
|
||
try:
|
||
self.db._sync_actions_studio_schema_and_rows()
|
||
logger.info("actions_studio schema/rows synced (non-destructive)")
|
||
except Exception as e:
|
||
logger.error(f"actions_studio sync failed: {e}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error syncing actions to database: {e}")
|
||
|
||
|
||
def _extract_action_metadata(self, filepath: str) -> Optional[Dict[str, Any]]:
|
||
"""Extract action metadata from Python file using AST parsing (Safe)"""
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
tree = ast.parse(f.read(), filename=filepath)
|
||
|
||
meta = {}
|
||
for node in tree.body:
|
||
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
||
if isinstance(node.targets[0], ast.Name):
|
||
key = node.targets[0].id
|
||
if key.startswith("b_"):
|
||
try:
|
||
val = ast.literal_eval(node.value)
|
||
meta[key] = val
|
||
except (ValueError, SyntaxError):
|
||
logger.warning(f"Could not safe-eval variable {key} in {filepath}. Use literals only.")
|
||
pass
|
||
|
||
# Set default module name if not specified
|
||
if "b_module" not in meta:
|
||
meta["b_module"] = os.path.splitext(os.path.basename(filepath))[0]
|
||
|
||
return meta if meta.get("b_class") else None
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to parse {filepath}: {e}")
|
||
return None
|
||
|
||
# ... (le reste des méthodes initialize_database, load_config, etc. reste inchangé) ...
|
||
# Assurez-vous d'inclure les autres méthodes existantes de la classe SharedData ici.
|
||
# Pour la brièveté de la réponse, je ne répète pas les méthodes non modifiées si elles sont identiques au fichier original.
|
||
# [INCLURE LE RESTE DU FICHIER SHARED.PY ORIGINAL ICI]
|
||
def initialize_database(self):
|
||
"""Initialize database schema"""
|
||
logger.info("Initializing database schema")
|
||
try:
|
||
self.db.ensure_schema()
|
||
|
||
# Update status list from database if empty
|
||
if not self.status_list:
|
||
actions = self.db.list_actions()
|
||
for action in actions:
|
||
if action.get("b_class"):
|
||
self.status_list.append(action["b_class"])
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error initializing database: {e}")
|
||
|
||
def load_config(self):
|
||
"""Load configuration from database"""
|
||
try:
|
||
logger.info("Loading configuration from database")
|
||
cfg = self.db.get_config()
|
||
|
||
if not cfg:
|
||
# Seed with defaults
|
||
self.db.save_config(self.default_config.copy())
|
||
cfg = self.db.get_config() or {}
|
||
|
||
# Merge with current config
|
||
self.config.update(cfg)
|
||
|
||
# Expose config as attributes for backward compatibility
|
||
for key, value in self.config.items():
|
||
setattr(self, key, value)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading configuration: {e}")
|
||
# Fallback to defaults
|
||
for key, value in self.config.items():
|
||
setattr(self, key, value)
|
||
|
||
def save_config(self):
|
||
"""Save configuration to database"""
|
||
logger.info("Saving configuration to database")
|
||
try:
|
||
self.db.save_config(self.config)
|
||
logger.info("Configuration saved successfully")
|
||
except Exception as e:
|
||
logger.error(f"Error saving configuration: {e}")
|
||
|
||
def load_fonts(self):
|
||
"""Load font resources"""
|
||
try:
|
||
logger.info("Loading fonts")
|
||
|
||
# Font paths
|
||
self.default_font_path = os.path.join(self.fonts_dir, self.defaultfont)
|
||
self.default_font_title_path = os.path.join(self.fonts_dir, self.defaultfonttitle)
|
||
|
||
# Load font sizes
|
||
self.font_arial14 = self._load_font(self.default_font_path, 14)
|
||
self.font_arial11 = self._load_font(self.default_font_path, 11)
|
||
self.font_arial9 = self._load_font(self.default_font_path, 9)
|
||
self.font_arialbold = self._load_font(self.default_font_path, 12)
|
||
|
||
# Viking font for title
|
||
self.font_viking_path = self.default_font_title_path
|
||
self.font_viking = self._load_font(self.default_font_title_path, 13)
|
||
|
||
logger.info("Fonts loaded successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading fonts: {e}")
|
||
raise
|
||
|
||
def _load_font(self, font_path: str, size: int):
|
||
"""Load a single font with specified size"""
|
||
try:
|
||
return ImageFont.truetype(font_path, size)
|
||
except Exception as e:
|
||
logger.error(f"Error loading font {font_path}: {e}")
|
||
raise
|
||
|
||
def load_images(self):
|
||
"""Load image resources for display"""
|
||
try:
|
||
logger.info("Loading images")
|
||
|
||
# Initialize status image
|
||
self.bjorn_status_image = None
|
||
|
||
# Load static images
|
||
self._load_static_images()
|
||
|
||
# Load status images
|
||
self._load_status_images()
|
||
|
||
# Calculate display positions
|
||
self._calculate_image_positions()
|
||
|
||
logger.info("Images loaded successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading images: {e}")
|
||
raise
|
||
|
||
def _load_static_images(self):
|
||
"""Load static UI images"""
|
||
static_images = {
|
||
'bjorn1': 'bjorn1.bmp',
|
||
'port': 'port.bmp',
|
||
'frise': 'frise.bmp',
|
||
'target': 'target.bmp',
|
||
'vuln': 'vuln.bmp',
|
||
'connected': 'connected.bmp',
|
||
'bluetooth': 'bluetooth.bmp',
|
||
'wifi': 'wifi.bmp',
|
||
'ethernet': 'ethernet.bmp',
|
||
'usb': 'usb.bmp',
|
||
'level': 'level.bmp',
|
||
'cred': 'cred.bmp',
|
||
'attack': 'attack.bmp',
|
||
'attacks': 'attacks.bmp',
|
||
'gold': 'gold.bmp',
|
||
'networkkb': 'networkkb.bmp',
|
||
'zombie': 'zombie.bmp',
|
||
'data': 'data.bmp',
|
||
'money': 'money.bmp',
|
||
'zombie_status': 'zombie.bmp',
|
||
'battery0': '0.bmp',
|
||
'battery25': '25.bmp',
|
||
'battery50': '50.bmp',
|
||
'battery75': '75.bmp',
|
||
'battery100': '100.bmp',
|
||
'battery_charging': 'charging1.bmp'
|
||
}
|
||
|
||
for attr_name, filename in static_images.items():
|
||
image_path = os.path.join(self.static_images_dir, filename)
|
||
setattr(self, attr_name, self._load_image(image_path))
|
||
|
||
def _load_status_images(self):
|
||
"""Load status-specific images and image series"""
|
||
self.image_series = {}
|
||
|
||
try:
|
||
# Load images from database actions
|
||
actions = self.db.list_actions()
|
||
for action in actions:
|
||
b_class = action.get('b_class')
|
||
if b_class:
|
||
# Load individual status image
|
||
status_dir = os.path.join(self.status_images_dir, b_class)
|
||
image_path = os.path.join(status_dir, f'{b_class}.bmp')
|
||
image = self._load_image(image_path)
|
||
setattr(self, b_class, image)
|
||
|
||
if b_class not in self.status_list:
|
||
self.status_list.append(b_class)
|
||
|
||
# Load image series for animations
|
||
self.image_series[b_class] = []
|
||
if not os.path.isdir(status_dir):
|
||
os.makedirs(status_dir, exist_ok=True)
|
||
logger.warning(f"Created missing directory: {status_dir}")
|
||
|
||
# Load numbered images for animation
|
||
for image_name in os.listdir(status_dir):
|
||
if image_name.endswith('.bmp') and re.search(r'\d', image_name):
|
||
series_image = self._load_image(os.path.join(status_dir, image_name))
|
||
if series_image:
|
||
self.image_series[b_class].append(series_image)
|
||
|
||
logger.info(f"Loaded {len(self.image_series.get(b_class, []))} images for {b_class}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading status images: {e}")
|
||
|
||
# Ensure IDLE images exist as fallback
|
||
if not self.image_series:
|
||
logger.error("No image series loaded")
|
||
else:
|
||
for status, images in self.image_series.items():
|
||
logger.info(f"Status {status}: {len(images)} animation frames")
|
||
|
||
def _load_image(self, image_path: str) -> Optional[Image.Image]:
|
||
"""Load a single image file"""
|
||
try:
|
||
if not os.path.exists(image_path):
|
||
logger.warning(f"Image not found: {image_path}")
|
||
return None
|
||
return Image.open(image_path)
|
||
except Exception as e:
|
||
logger.error(f"Error loading image {image_path}: {e}")
|
||
return None
|
||
|
||
def _calculate_image_positions(self):
|
||
"""Calculate image positions for display centering"""
|
||
if self.bjorn1:
|
||
self.x_center1 = (self.width - self.bjorn1.width) // 2
|
||
self.y_bottom1 = self.height - self.bjorn1.height
|
||
|
||
def update_bjorn_status(self):
|
||
"""Update current status image"""
|
||
try:
|
||
self.bjorn_status_image = getattr(self, self.bjorn_orch_status, None)
|
||
if self.bjorn_status_image is None:
|
||
logger.warning(f"Image for status {self.bjorn_orch_status} not available, using default")
|
||
self.bjorn_status_image = self.attack
|
||
except AttributeError:
|
||
logger.warning(f"Status {self.bjorn_orch_status} not found, using IDLE")
|
||
self.bjorn_status_image = self.attack
|
||
|
||
self.bjorn_status_text = self.bjorn_orch_status
|
||
|
||
def update_image_randomizer(self):
|
||
"""Select random image from current status series"""
|
||
try:
|
||
status = self.bjorn_status_text
|
||
|
||
# Try to get images for current status
|
||
if status in self.image_series and self.image_series[status]:
|
||
images = self.image_series[status]
|
||
# Fallback to IDLE images
|
||
elif "IDLE" in self.image_series and self.image_series["IDLE"]:
|
||
logger.warning(f"No images for {status}, using IDLE")
|
||
images = self.image_series["IDLE"]
|
||
else:
|
||
logger.error("No images available")
|
||
self.imagegen = None
|
||
return
|
||
|
||
# Select random image
|
||
random_index = random.randint(0, len(images) - 1)
|
||
self.imagegen = images[random_index]
|
||
|
||
# Calculate centering
|
||
self.x_center = (self.width - self.imagegen.width) // 2
|
||
self.y_bottom = self.height - self.imagegen.height
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating image randomizer: {e}")
|
||
self.imagegen = None
|
||
|
||
def wrap_text(self, text: str, font: ImageFont.FreeTypeFont, max_width: int) -> List[str]:
|
||
"""Wrap text to fit within specified width"""
|
||
try:
|
||
lines = []
|
||
words = text.split()
|
||
|
||
while words:
|
||
line = []
|
||
while words and font.getlength(' '.join(line + [words[0]])) <= max_width:
|
||
line.append(words.pop(0))
|
||
lines.append(' '.join(line).strip())
|
||
|
||
return lines
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error wrapping text: {e}")
|
||
return [text]
|
||
|
||
def update_stats(self):
|
||
"""Update calculated statistics based on formulas"""
|
||
self.coin_count = int(
|
||
self.network_kb_count * 5 +
|
||
self.cred_count * 5 +
|
||
self.data_count * 5 +
|
||
self.zombie_count * 10 +
|
||
self.attacks_count * 5 +
|
||
self.vuln_count * 2
|
||
)
|
||
|
||
self.level_count = int(
|
||
self.network_kb_count * 0.1 +
|
||
self.cred_count * 0.2 +
|
||
self.data_count * 0.1 +
|
||
self.zombie_count * 0.5 +
|
||
self.attacks_count +
|
||
self.vuln_count * 0.01
|
||
)
|
||
|
||
def debug_print(self, message: str):
|
||
"""Print debug message if debug mode is enabled"""
|
||
if self.config.get('debug_mode', False):
|
||
logger.debug(message) |