diff --git a/src/handler.py b/src/handler.py index bed3369..9d8abe2 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 +import logging import random import time from http.server import BaseHTTPRequestHandler from typing import Optional, List -from datetime import datetime from config import Config from tracker import AccessTracker @@ -23,6 +23,8 @@ class Handler(BaseHTTPRequestHandler): config: Config = None tracker: AccessTracker = None counter: int = 0 + app_logger: logging.Logger = None + access_logger: logging.Logger = None def _get_client_ip(self) -> str: """Extract client IP address from request, checking proxy headers first""" @@ -202,14 +204,14 @@ class Handler(BaseHTTPRequestHandler): client_ip = self._get_client_ip() user_agent = self._get_user_agent() post_data = "" - - print(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}") - + + self.access_logger.warning(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}") + content_length = int(self.headers.get('Content-Length', 0)) if content_length > 0: post_data = self.rfile.read(content_length).decode('utf-8', errors="replace") - - print(f"[POST DATA] {post_data[:200]}") + + self.access_logger.warning(f"[POST DATA] {post_data[:200]}") # send the post data (body) to the record_access function so the post data can be used to detect suspicious things. self.tracker.record_access(client_ip, self.path, user_agent, post_data) @@ -226,7 +228,7 @@ class Handler(BaseHTTPRequestHandler): pass except Exception as e: # Log other exceptions but don't crash - print(f"[ERROR] Failed to send response to {client_ip}: {str(e)}") + self.app_logger.error(f"Failed to send response to {client_ip}: {str(e)}") def serve_special_path(self, path: str) -> bool: """Serve special paths like robots.txt, API endpoints, etc.""" @@ -307,9 +309,9 @@ class Handler(BaseHTTPRequestHandler): # Client disconnected, ignore silently pass except Exception as e: - print(f"[ERROR] Failed to serve special path {path}: {str(e)}") + self.app_logger.error(f"Failed to serve special path {path}: {str(e)}") pass - + return False def do_GET(self): @@ -327,17 +329,17 @@ class Handler(BaseHTTPRequestHandler): except BrokenPipeError: pass except Exception as e: - print(f"Error generating dashboard: {e}") + self.app_logger.error(f"Error generating dashboard: {e}") return self.tracker.record_access(client_ip, self.path, user_agent) if self.tracker.is_suspicious_user_agent(user_agent): - print(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}") + self.access_logger.warning(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}") if self._should_return_error(): error_code = self._get_random_error_code() - print(f"[ERROR] Returning {error_code} to {client_ip} - {self.path}") + self.access_logger.info(f"Returning error {error_code} to {client_ip} - {self.path}") self.send_response(error_code) self.end_headers() return @@ -361,9 +363,9 @@ class Handler(BaseHTTPRequestHandler): # Client disconnected, ignore silently pass except Exception as e: - print(f"Error generating page: {e}") + self.app_logger.error(f"Error generating page: {e}") def log_message(self, format, *args): - """Override to customize logging""" + """Override to customize logging - uses access logger""" client_ip = self._get_client_ip() - print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {client_ip} - {format % args}") + self.access_logger.info(f"{client_ip} - {format % args}") diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..68b8278 --- /dev/null +++ b/src/logger.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +""" +Logging singleton module for the Krawl honeypot. +Provides two loggers: app (application) and access (HTTP access logs). +""" + +import logging +import os +from logging.handlers import RotatingFileHandler + + +class LoggerManager: + """Singleton logger manager for the application.""" + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def initialize(self, log_dir: str = "logs") -> None: + """ + Initialize the logging system with rotating file handlers. + + Args: + log_dir: Directory for log files (created if not exists) + """ + if self._initialized: + return + + # Create log directory if it doesn't exist + os.makedirs(log_dir, exist_ok=True) + + # Common format for all loggers + log_format = logging.Formatter( + "[%(asctime)s] %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + # Rotation settings: 1MB max, 5 backups + max_bytes = 1048576 # 1MB + backup_count = 5 + + # Setup application logger + self._app_logger = logging.getLogger("krawl.app") + self._app_logger.setLevel(logging.INFO) + self._app_logger.handlers.clear() + + app_file_handler = RotatingFileHandler( + os.path.join(log_dir, "krawl.log"), + maxBytes=max_bytes, + backupCount=backup_count + ) + app_file_handler.setFormatter(log_format) + self._app_logger.addHandler(app_file_handler) + + app_stream_handler = logging.StreamHandler() + app_stream_handler.setFormatter(log_format) + self._app_logger.addHandler(app_stream_handler) + + # Setup access logger + self._access_logger = logging.getLogger("krawl.access") + self._access_logger.setLevel(logging.INFO) + self._access_logger.handlers.clear() + + access_file_handler = RotatingFileHandler( + os.path.join(log_dir, "access.log"), + maxBytes=max_bytes, + backupCount=backup_count + ) + access_file_handler.setFormatter(log_format) + self._access_logger.addHandler(access_file_handler) + + access_stream_handler = logging.StreamHandler() + access_stream_handler.setFormatter(log_format) + self._access_logger.addHandler(access_stream_handler) + + self._initialized = True + + @property + def app(self) -> logging.Logger: + """Get the application logger.""" + if not self._initialized: + self.initialize() + return self._app_logger + + @property + def access(self) -> logging.Logger: + """Get the access logger.""" + if not self._initialized: + self.initialize() + return self._access_logger + + +# Module-level singleton instance +_logger_manager = LoggerManager() + + +def get_app_logger() -> logging.Logger: + """Get the application logger instance.""" + return _logger_manager.app + + +def get_access_logger() -> logging.Logger: + """Get the access logger instance.""" + return _logger_manager.access + + +def initialize_logging(log_dir: str = "logs") -> None: + """Initialize the logging system.""" + _logger_manager.initialize(log_dir) diff --git a/src/server.py b/src/server.py index 73f0ce9..861e9f2 100644 --- a/src/server.py +++ b/src/server.py @@ -11,6 +11,7 @@ from http.server import HTTPServer from config import Config from tracker import AccessTracker from handler import Handler +from logger import initialize_logging, get_app_logger, get_access_logger def print_usage(): @@ -40,13 +41,20 @@ def main(): print_usage() exit(0) + # Initialize logging + initialize_logging() + app_logger = get_app_logger() + access_logger = get_access_logger() + config = Config.from_env() - + tracker = AccessTracker() - + Handler.config = config Handler.tracker = tracker Handler.counter = config.canary_token_tries + Handler.app_logger = app_logger + Handler.access_logger = access_logger if len(sys.argv) == 2: try: @@ -54,29 +62,29 @@ def main(): Handler.webpages = f.readlines() if not Handler.webpages: - print('The file provided was empty. Using randomly generated links.') + app_logger.warning('The file provided was empty. Using randomly generated links.') Handler.webpages = None except IOError: - print('Can\'t read input file. Using randomly generated links.') + app_logger.warning("Can't read input file. Using randomly generated links.") try: - print(f'Starting deception server on port {config.port}...') - print(f'Dashboard available at: {config.dashboard_secret_path}') + app_logger.info(f'Starting deception server on port {config.port}...') + app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}') if config.canary_token_url: - print(f'Canary token will appear after {config.canary_token_tries} tries') + app_logger.info(f'Canary token will appear after {config.canary_token_tries} tries') else: - print('No canary token configured (set CANARY_TOKEN_URL to enable)') - + app_logger.info('No canary token configured (set CANARY_TOKEN_URL to enable)') + server = HTTPServer(('0.0.0.0', config.port), Handler) - print('Server started. Use to stop.') + app_logger.info('Server started. Use to stop.') server.serve_forever() except KeyboardInterrupt: - print('\nStopping server...') + app_logger.info('Stopping server...') server.socket.close() - print('Server stopped') + app_logger.info('Server stopped') except Exception as e: - print(f'Error starting HTTP server on port {config.port}: {e}') - print(f'Make sure you are root, if needed, and that port {config.port} is open.') + app_logger.error(f'Error starting HTTP server on port {config.port}: {e}') + app_logger.error(f'Make sure you are root, if needed, and that port {config.port} is open.') exit(1) diff --git a/src/wordlists.py b/src/wordlists.py index b0a9e1a..62e4045 100644 --- a/src/wordlists.py +++ b/src/wordlists.py @@ -6,9 +6,10 @@ This allows easy customization without touching Python code. """ import json -import os from pathlib import Path +from logger import get_app_logger + class Wordlists: """Loads and provides access to wordlists from wordlists.json""" @@ -19,15 +20,15 @@ class Wordlists: def _load_config(self): """Load wordlists from JSON file""" config_path = Path(__file__).parent.parent / 'wordlists.json' - + try: with open(config_path, 'r') as f: return json.load(f) except FileNotFoundError: - print(f"⚠️ Warning: {config_path} not found, using default values") + get_app_logger().warning(f"Wordlists file {config_path} not found, using default values") return self._get_defaults() except json.JSONDecodeError as e: - print(f"⚠️ Warning: Invalid JSON in {config_path}: {e}") + get_app_logger().warning(f"Invalid JSON in {config_path}: {e}") return self._get_defaults() def _get_defaults(self):