From 122a3633c27f93d2b8df13608f6b78eeef7b38b8 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Dec 2025 22:13:41 +0000 Subject: [PATCH] APP_ENV removal and secure cookie handling (#333) Hey, made the tweaks we discussed, plus a couple related fixes :) - Removed APP_ENV entirely. All dev-specific functionality is enabled via `DEBUG: true` env var - Set secure cookie handling to false by default, added to the readme to enable if exclusively using HTTPS connection - Fixed healthcheck potentially not working with auth enabled - Removed APP_ENV from docker compose files and made sure app.db lines are included in all versions. APP_ENV in people's existing composes should get ignored entirely and will be put on the default env, so no issues when updating. --- Dockerfile | 7 ++--- app.py | 41 ++++++++++++++------------ docker-compose.dev.yml | 1 - docker-compose.extbp.dev.yml | 1 - docker-compose.extbp.yml | 6 ++-- docker-compose.tor.dev.yml | 1 - docker-compose.tor.yml | 11 +++++-- docker-compose.yml | 8 +++-- entrypoint.sh | 13 ++++---- env.py | 7 +---- readme.md | 6 +++- src/frontend/src/App.tsx | 6 ++-- src/frontend/src/components/Footer.tsx | 14 ++++++--- src/frontend/src/types/index.ts | 1 - 14 files changed, 68 insertions(+), 55 deletions(-) diff --git a/Dockerfile b/Dockerfile index 819873ad..7a974a55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,8 +49,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ # UID/GID will be handled by entrypoint script, but TZ/Locale are still needed LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ - LC_ALL=en_US.UTF-8 \ - APP_ENV=dev + LC_ALL=en_US.UTF-8 # Set ARG for build-time expansion (FLASK_PORT), ENV for runtime access ENV FLASK_PORT=8084 @@ -108,9 +107,9 @@ RUN mkdir -p /var/log/cwa-book-downloader /cwa-book-ingest && \ EXPOSE ${FLASK_PORT} # Add healthcheck for container status -# This will run as root initially, but check localhost which should work if the app binds correctly. +# Uses /api/health which doesn't require authentication HEALTHCHECK --interval=60s --timeout=60s --start-period=60s --retries=3 \ - CMD curl -s http://localhost:${FLASK_PORT}/api/status > /dev/null || exit 1 + CMD curl -s http://localhost:${FLASK_PORT}/api/health > /dev/null || exit 1 # Use dumb-init as the entrypoint to handle signals properly ENTRYPOINT ["/usr/bin/dumb-init", "--"] diff --git a/app.py b/app.py index b7eb94e7..83127bff 100644 --- a/app.py +++ b/app.py @@ -16,7 +16,7 @@ import typing from logger import setup_logger from config import _SUPPORTED_BOOK_LANGUAGE, BOOK_LANGUAGE, SUPPORTED_FORMATS -from env import FLASK_HOST, FLASK_PORT, APP_ENV, CWA_DB_PATH, DEBUG, USING_EXTERNAL_BYPASSER, BUILD_VERSION, RELEASE_VERSION, CALIBRE_WEB_URL +from env import FLASK_HOST, FLASK_PORT, CWA_DB_PATH, DEBUG, USING_EXTERNAL_BYPASSER, BUILD_VERSION, RELEASE_VERSION, CALIBRE_WEB_URL import backend from models import SearchFilters @@ -28,13 +28,13 @@ app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching app.config['APPLICATION_ROOT'] = '/' -# Determine async mode based on environment -# In production with Gunicorn + gevent worker, use 'gevent' -# In development with Flask dev server, use 'threading' -if APP_ENV == 'prod': - async_mode = 'gevent' -else: +# Determine async mode based on DEBUG setting +# In production (DEBUG=False) with Gunicorn + gevent worker, use 'gevent' +# In development (DEBUG=True) with Flask dev server, use 'threading' +if DEBUG: async_mode = 'threading' +else: + async_mode = 'gevent' # Initialize Flask-SocketIO with reverse proxy support socketio = SocketIO( @@ -152,16 +152,9 @@ werkzeug_logger.addFilter(StatusEndpointFilter()) # The secret key will reset every time we restart, which will # require users to authenticate again -# Secure cookie handling (HTTP vs HTTPS) -# Can be overridden with SESSION_COOKIE_SECURE environment variable -session_cookie_secure_env = os.getenv('SESSION_COOKIE_SECURE', 'auto').lower() -if session_cookie_secure_env in ['true', 'yes', '1']: - SESSION_COOKIE_SECURE = True -elif session_cookie_secure_env in ['false', 'no', '0']: - SESSION_COOKIE_SECURE = False -else: - # Auto mode: align with deployment environment - SESSION_COOKIE_SECURE = APP_ENV == 'prod' +# Session cookie security - set to 'true' if exclusively using HTTPS +session_cookie_secure_env = os.getenv('SESSION_COOKIE_SECURE', 'false').lower() +SESSION_COOKIE_SECURE = session_cookie_secure_env in ['true', 'yes', '1'] app.config.update( SECRET_KEY = os.urandom(64), @@ -382,7 +375,6 @@ def api_config() -> Union[Response, Tuple[Response, int]]: config = { "calibre_web_url": CALIBRE_WEB_URL, "debug": DEBUG, - "app_env": APP_ENV, "build_version": BUILD_VERSION, "release_version": RELEASE_VERSION, "book_languages": _SUPPORTED_BOOK_LANGUAGE, @@ -394,6 +386,17 @@ def api_config() -> Union[Response, Tuple[Response, int]]: logger.error_trace(f"Config error: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/health', methods=['GET']) +def api_health() -> Union[Response, Tuple[Response, int]]: + """ + Health check endpoint for container orchestration. + No authentication required. + + Returns: + flask.Response: JSON with status "ok". + """ + return jsonify({"status": "ok"}) + @app.route('/api/status', methods=['GET']) @login_required def api_status() -> Union[Response, Tuple[Response, int]]: @@ -812,7 +815,7 @@ def handle_status_request(): logger.log_resource_usage() if __name__ == '__main__': - logger.info(f"Starting Flask application with WebSocket support on {FLASK_HOST}:{FLASK_PORT} IN {APP_ENV} mode") + logger.info(f"Starting Flask application with WebSocket support on {FLASK_HOST}:{FLASK_PORT} (debug={DEBUG})") socketio.run( app, host=FLASK_HOST, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 18ea5566..3cb759ce 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,7 +9,6 @@ services: target: cwa-bd environment: DEBUG: true - APP_ENV: dev USE_DOH: true CUSTOM_DNS: cloudflare volumes: diff --git a/docker-compose.extbp.dev.yml b/docker-compose.extbp.dev.yml index c18e3450..85dd4a2c 100644 --- a/docker-compose.extbp.dev.yml +++ b/docker-compose.extbp.dev.yml @@ -10,7 +10,6 @@ services: target: cwa-bd-extbp environment: DEBUG: true - APP_ENV: dev USE_DOH: true CUSTOM_DNS: cloudflare USE_CF_BYPASS: true # Enable Cloudflare bypass (default: true) diff --git a/docker-compose.extbp.yml b/docker-compose.extbp.yml index cec48a98..6893fb74 100644 --- a/docker-compose.extbp.yml +++ b/docker-compose.extbp.yml @@ -7,9 +7,11 @@ services: BOOK_LANGUAGE: en USE_BOOK_TITLE: true TZ: America/New_York - APP_ENV: prod UID: 1000 GID: 100 + # CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication + # SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS + # DEBUG: 'true' # Enable debug mode (debug button, verbose logging) EXT_BYPASSER_URL: http://flaresolverr:8191 ports: - 8084:8084 @@ -19,7 +21,7 @@ services: # the same as whatever you gave in "calibre-web-automated" - /tmp/data/calibre-web/ingest:/cwa-book-ingest # This is the location of CWA's app.db, which contains authentication - # details + # details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above) #- /cwa/config/path/app.db:/auth/app.db:ro flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest diff --git a/docker-compose.tor.dev.yml b/docker-compose.tor.dev.yml index 920cba31..1eafee66 100644 --- a/docker-compose.tor.dev.yml +++ b/docker-compose.tor.dev.yml @@ -9,7 +9,6 @@ services: target: cwa-bd-tor environment: DEBUG: true - APP_ENV: dev volumes: - /tmp/cwa-book-downloader:/tmp/cwa-book-downloader - /tmp/cwa-book-downloader-log:/var/log/cwa-book-downloader diff --git a/docker-compose.tor.yml b/docker-compose.tor.yml index 7d7c2b58..9e361e83 100644 --- a/docker-compose.tor.yml +++ b/docker-compose.tor.yml @@ -8,7 +8,9 @@ services: USE_BOOK_TITLE: true TZ: America/New_York USING_TOR: true - APP_ENV: prod + # CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication + # SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS + # DEBUG: 'true' # Enable debug mode (debug button, verbose logging) cap_add: - NET_ADMIN - NET_RAW @@ -16,6 +18,9 @@ services: - 8084:8084 restart: unless-stopped volumes: - # This is where the books will be downloaded to, usually it would be - # the same as whatever you gave in "calibre-web-automated" + # This is where the books will be downloaded to, usually it would be + # the same as whatever you gave in "calibre-web-automated" - /tmp/data/calibre-web/ingest:/cwa-book-ingest + # This is the location of CWA's app.db, which contains authentication + # details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above) + #- /cwa/config/path/app.db:/auth/app.db:ro diff --git a/docker-compose.yml b/docker-compose.yml index f3950c42..09fdd1c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,10 +11,12 @@ services: BOOK_LANGUAGE: en USE_BOOK_TITLE: true TZ: America/New_York - APP_ENV: prod UID: 1000 GID: 100 - # CWA_DB_PATH: /auth/app.db # Comment out to disable authentication + # CWA_DB_PATH: /auth/app.db # Uncomment to enable authentication (also uncomment volume below) + # CALIBRE_WEB_URL: http://localhost:8080 # Uncomment and add your custom library URL to enable "Go To Library" button in the Web UI + # SESSION_COOKIE_SECURE: 'true' # Set to 'true' if accessing ONLY via HTTPS + # DEBUG: 'true' # Enable debug mode (debug button, verbose logging) # Queue management settings MAX_CONCURRENT_DOWNLOADS: 3 DOWNLOAD_PROGRESS_UPDATE_INTERVAL: 5 @@ -26,5 +28,5 @@ services: # the same as whatever you gave in "calibre-web-automated" - /tmp/data/calibre-web/ingest:/cwa-book-ingest # This is the location of CWA's app.db, which contains authentication - # details. Comment out to disable authentication + # details. Uncomment to enable authentication (also uncomment CWA_DB_PATH above) #- /cwa/config/path/app.db:/auth/app.db:ro diff --git a/entrypoint.sh b/entrypoint.sh index 9c813846..6872fa1c 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -105,16 +105,17 @@ change_ownership /tmp/cwa-book-downloader # Test write to all folders make_writable /cwa-book-ingest -# Set the command to run based on the environment -is_prod=$(echo "$APP_ENV" | tr '[:upper:]' '[:lower:]') -if [ "$is_prod" = "prod" ]; then +# Set the command to run based on DEBUG setting +# DEBUG=true uses Flask dev server, otherwise uses gunicorn for production +is_debug=$(echo "$DEBUG" | tr '[:upper:]' '[:lower:]') +if [ "$is_debug" = "true" ]; then + command="python3 app.py" +else # Use geventwebsocket worker for SocketIO + WebSocket compatibility # This special worker class handles WebSocket upgrades properly # --workers 1: SocketIO requires sticky sessions, use 1 worker or configure sticky sessions # -t 300: 300 second timeout for long-running requests command="gunicorn --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} app:app" -else - command="python3 app.py" fi # If DEBUG and not using an external bypass @@ -179,7 +180,7 @@ sum=$(python3 -c "print(sum(int(l.strip()) for l in open('/tmp/test.cwa-bd').rea [ "$sum" == 11250075000 ] && echo "Success: /tmp is writable" || (echo "Failure: /tmp is not writable" && exit 1) rm /tmp/test.cwa-bd -echo "Running command: '$command' as '$USERNAME' in '$APP_ENV' mode" +echo "Running command: '$command' as '$USERNAME' (debug=$is_debug)" # Stop logging exec 1>&3 2>&4 diff --git a/env.py b/env.py index 6d010179..a920d226 100644 --- a/env.py +++ b/env.py @@ -5,11 +5,7 @@ def string_to_bool(s: str) -> bool: return s.lower() in ["true", "yes", "1", "y"] # Authentication and session settings -# SESSION_COOKIE_SECURE: Controls whether session cookies are marked as secure (HTTPS only) -# - 'auto' (default): Uses False in dev, True in prod, can be overridden with environment variable -# - 'true'/'yes'/'1': Always use secure cookies (recommended for production with HTTPS) -# - 'false'/'no'/'0': Never use secure cookies (only for local HTTP) -SESSION_COOKIE_SECURE_ENV = os.getenv("SESSION_COOKIE_SECURE", "auto") +SESSION_COOKIE_SECURE_ENV = os.getenv("SESSION_COOKIE_SECURE", "false") CWA_DB = os.getenv("CWA_DB_PATH") CWA_DB_PATH = Path(CWA_DB) if CWA_DB else None @@ -54,7 +50,6 @@ _CUSTOM_SCRIPT = os.getenv("CUSTOM_SCRIPT", "").strip() FLASK_HOST = os.getenv("FLASK_HOST", "0.0.0.0") FLASK_PORT = int(os.getenv("FLASK_PORT", "8084")) DEBUG = string_to_bool(os.getenv("DEBUG", "false")) -APP_ENV = os.getenv("APP_ENV", "N/A").lower() PRIORITIZE_WELIB = string_to_bool(os.getenv("PRIORITIZE_WELIB", "false")) ALLOW_USE_WELIB = string_to_bool(os.getenv("ALLOW_USE_WELIB", "true")) diff --git a/readme.md b/readme.md index 37dcff31..a66260cb 100644 --- a/readme.md +++ b/readme.md @@ -63,10 +63,14 @@ An intuitive web interface for searching and requesting book downloads, designed | `CWA_DB_PATH` | Calibre-Web's database | None | | `ENABLE_LOGGING` | Enable log file | `true` | | `LOG_LEVEL` | Log level to use | `info` | +| `SESSION_COOKIE_SECURE` | Secure cookie enforcement - Use for HTTPS connections only | `false` | +| `CALIBRE_WEB_URL` | Custom WebUI library link | None | If you wish to enable authentication, you must set `CWA_DB_PATH` to point to Calibre-Web's `app.db`, in order to match the username and password. -If logging is enabld, log folder default location is `/var/log/cwa-book-downloader` +Set `CALIBRE_WEB_URL` to your Calibre-Web / Booklore base URL. A ‘Go to library’ button will appear in the Web UI for quick access while downloading, and it also provides library access when CWA-BD is installed as a mobile PWA. + +If logging is enabled, log folder default location is `/var/log/cwa-book-downloader` Available log levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Higher levels show fewer messages. Note that if using TOR, the TZ will be calculated automatically based on IP. diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index fb6db7bd..6db8d871 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -490,9 +490,9 @@ function App() {