diff --git a/Dockerfile b/Dockerfile index e93a55c..fa2f076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,16 +15,18 @@ RUN pip install --no-cache-dir --upgrade pip && \ COPY src/ /app/src/ COPY wordlists.json /app/ COPY entrypoint.sh /app/ +COPY start.sh /app/ COPY config.yaml /app/ RUN useradd -m -u 1000 krawl && \ mkdir -p /app/logs /app/data /app/exports && \ chown -R krawl:krawl /app && \ - chmod +x /app/entrypoint.sh + chmod +x /app/entrypoint.sh /app/start.sh -EXPOSE 5000 +EXPOSE 5010 +EXPOSE 5123 ENV PYTHONUNBUFFERED=1 ENTRYPOINT ["/app/entrypoint.sh"] -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5000", "--app-dir", "src"] +CMD ["/app/start.sh"] diff --git a/config.yaml b/config.yaml index cef8e52..9e21b45 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # Krawl Honeypot Configuration server: - port: 5000 + port: 5010 delay: 100 # Response delay in milliseconds # manually set the server header, if null a random one will be used. @@ -20,10 +20,9 @@ canary: token_tries: 10 dashboard: - # if set to "null" this will Auto-generates random path if not set - # can be set to "/dashboard" or similar <-- note this MUST include a forward slash - # secret_path: super-secret-dashboard-path - secret_path: null + port: 5123 + # Set to empty string "" to serve dashboard at root "/" on its own dedicated port + secret_path: "" # Password for accessing protected dashboard panels. # If null, a random password will be generated and printed in the logs. diff --git a/docker-compose.yaml b/docker-compose.yaml index feab0f5..428d85d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,4 @@ --- -# THIS IS FOR DEVELOPMENT PURPOSES services: krawl: build: @@ -7,12 +6,15 @@ services: dockerfile: Dockerfile container_name: krawl-server ports: - - "5000:5000" + - "5010:5010" # Honeypot (crawler trap) + - "5123:5123" # Dashboard (public UI) environment: - CONFIG_LOCATION=config.yaml # Uncomment to set a custom dashboard password (auto-generated if not set) # - KRAWL_DASHBOARD_PASSWORD=your-secret-password - # set this to change timezone, alternatively mount /etc/timezone or /etc/localtime based on the time system management of the host environment + # Override ports if needed + # - HONEYPOT_PORT=5010 + # - DASHBOARD_PORT=5123 # - TZ=${TZ} volumes: - ./wordlists.json:/app/wordlists.json:ro diff --git a/src/app.py b/src/app.py index d3d8e7b..6f1ca86 100644 --- a/src/app.py +++ b/src/app.py @@ -10,7 +10,6 @@ import os from contextlib import asynccontextmanager from fastapi import FastAPI, Request, Response -from fastapi.staticfiles import StaticFiles from config import get_config from tracker import AccessTracker, set_tracker @@ -72,21 +71,7 @@ async def lifespan(app: FastAPI): tasks_master = get_tasksmaster() tasks_master.run_scheduled_tasks() - password_line = "" - if config.dashboard_password_generated: - password_line = ( - f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}" - ) - - banner = f""" - -============================================================ -DASHBOARD AVAILABLE AT -{config.dashboard_secret_path}{password_line} -============================================================ - """ - app_logger.info(banner) - app_logger.info(f"Starting deception server on port {config.port}...") + app_logger.info(f"Starting honeypot deception server on port {config.port}...") if config.canary_token_url: app_logger.info( f"Canary token will appear after {config.canary_token_tries} tries" @@ -155,29 +140,10 @@ def create_app() -> FastAPI: access_logger.info(f"[{method}] {client_ip} - {path} - {status}") return response - # Mount static files for the dashboard - config = get_config() - secret = config.dashboard_secret_path.lstrip("/") - static_dir = os.path.join(os.path.dirname(__file__), "templates", "static") - application.mount( - f"/{secret}/static", - StaticFiles(directory=static_dir), - name="dashboard-static", - ) - # Import and include routers from routes.honeypot import router as honeypot_router - from routes.api import router as api_router - from routes.dashboard import router as dashboard_router - from routes.htmx import router as htmx_router - # Dashboard/API/HTMX routes (prefixed with secret path, before honeypot catch-all) - dashboard_prefix = f"/{secret}" - application.include_router(dashboard_router, prefix=dashboard_prefix) - application.include_router(api_router, prefix=dashboard_prefix) - application.include_router(htmx_router, prefix=dashboard_prefix) - - # Honeypot routes (catch-all must be last) + # Honeypot routes (catch-all) application.include_router(honeypot_router) return application diff --git a/src/app_dashboard.py b/src/app_dashboard.py new file mode 100644 index 0000000..2d62895 --- /dev/null +++ b/src/app_dashboard.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +""" +FastAPI application for the Krawl dashboard. +Runs on a dedicated port (default 5123) and serves the dashboard at /. +""" + +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request, Response +from fastapi.staticfiles import StaticFiles + +from config import get_config +from database import initialize_database +from tasks_master import get_tasksmaster +from logger import initialize_logging, get_app_logger + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Dashboard application startup and shutdown lifecycle.""" + config = get_config() + # Force dashboard path to empty (root) for this dedicated app + config.dashboard_secret_path = "" + + initialize_logging(log_level=config.log_level) + app_logger = get_app_logger() + + try: + app_logger.info(f"Initializing database at: {config.database_path}") + initialize_database(config.database_path) + app_logger.info("Database ready") + except Exception as e: + app_logger.warning( + f"Database initialization failed: {e}. Continuing with in-memory only." + ) + + app.state.config = config + + # Start scheduled tasks (handles cache warming, backups, etc.) + tasks_master = get_tasksmaster() + tasks_master.run_scheduled_tasks() + + password_line = "" + if config.dashboard_password_generated: + password_line = ( + f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}" + ) + + banner = f""" + +============================================================ +DASHBOARD AVAILABLE AT http://0.0.0.0:{config.dashboard_port}/{password_line} +============================================================ + """ + app_logger.info(banner) + app_logger.info(f"Starting dashboard server on port {config.dashboard_port}...") + + yield + + app_logger.info("Dashboard server shutting down...") + + +def create_dashboard_app() -> FastAPI: + """Create and configure the dashboard FastAPI application.""" + application = FastAPI( + docs_url=None, + redoc_url=None, + openapi_url=None, + lifespan=lifespan, + ) + + # Mount static files at /static + static_dir = os.path.join(os.path.dirname(__file__), "templates", "static") + application.mount( + "/static", + StaticFiles(directory=static_dir), + name="dashboard-static", + ) + + from routes.api import router as api_router + from routes.dashboard import router as dashboard_router + from routes.htmx import router as htmx_router + + # All dashboard routes at root prefix + application.include_router(dashboard_router, prefix="") + application.include_router(api_router, prefix="") + application.include_router(htmx_router, prefix="") + + return application + + +app = create_dashboard_app() diff --git a/src/config.py b/src/config.py index be0cf93..cb2c4c8 100644 --- a/src/config.py +++ b/src/config.py @@ -18,7 +18,8 @@ import yaml class Config: """Configuration class for the deception server""" - port: int = 5000 + port: int = 5010 + dashboard_port: int = 5123 delay: int = 100 # milliseconds server_header: str = "" links_length_range: Tuple[int, int] = (5, 15) @@ -173,6 +174,8 @@ class Config: dashboard_path = dashboard.get("secret_path") if dashboard_path is None: dashboard_path = f"/{os.urandom(16).hex()}" + elif dashboard_path == "": + dashboard_path = "" else: # ensure the dashboard path starts with a / if dashboard_path[:1] != "/": @@ -186,7 +189,8 @@ class Config: dashboard_password_generated = True return cls( - port=server.get("port", 5000), + port=server.get("port", 5010), + dashboard_port=dashboard.get("port", 5123), delay=server.get("delay", 100), server_header=server.get("server_header", ""), links_length_range=( diff --git a/src/routes/dashboard.py b/src/routes/dashboard.py index 37f9d51..e9c150f 100644 --- a/src/routes/dashboard.py +++ b/src/routes/dashboard.py @@ -19,7 +19,8 @@ router = APIRouter() @router.get("/") async def dashboard_page(request: Request): config = request.app.state.config - dashboard_path = "/" + config.dashboard_secret_path.lstrip("/") + _path = config.dashboard_secret_path.strip("/") + dashboard_path = f"/{_path}" if _path else "" # Serve from pre-computed cache when available, fall back to live queries if is_warm(): @@ -50,7 +51,8 @@ async def ip_page(ip_address: str, request: Request): try: stats = db.get_ip_stats_by_ip(ip_address) config = request.app.state.config - dashboard_path = "/" + config.dashboard_secret_path.lstrip("/") + _path = config.dashboard_secret_path.strip("/") + dashboard_path = f"/{_path}" if _path else "" if stats: # Transform fields for template compatibility diff --git a/src/routes/htmx.py b/src/routes/htmx.py index 7452e8a..5c8cd76 100644 --- a/src/routes/htmx.py +++ b/src/routes/htmx.py @@ -17,7 +17,8 @@ router = APIRouter() def _dashboard_path(request: Request) -> str: config = request.app.state.config - return "/" + config.dashboard_secret_path.lstrip("/") + path = config.dashboard_secret_path.strip("/") + return f"/{path}" if path else "" # ── Honeypot Triggers ──────────────────────────────────────────────── diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..e337a2e --- /dev/null +++ b/start.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e + +HONEYPOT_PORT=${HONEYPOT_PORT:-5010} +DASHBOARD_PORT=${DASHBOARD_PORT:-5123} + +echo "Starting Krawl honeypot on port $HONEYPOT_PORT..." +uvicorn app:app --host 0.0.0.0 --port "$HONEYPOT_PORT" --app-dir src & +HONEYPOT_PID=$! + +echo "Starting Krawl dashboard on port $DASHBOARD_PORT..." +uvicorn app_dashboard:app --host 0.0.0.0 --port "$DASHBOARD_PORT" --app-dir src & +DASHBOARD_PID=$! + +# Wait for either process to exit; if one dies, kill the other +wait -n 2>/dev/null || wait +kill $HONEYPOT_PID $DASHBOARD_PID 2>/dev/null || true