Compare commits

...
3 Commits
Author SHA1 Message Date
CaliBrain b293bee5f4 Always run Gunicorn (even when DEBUG=true) to ensure Socket.IO WebSocket (#341) 2025-12-13 00:21:44 -05:00
Alex 122a3633c2 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.
2025-12-11 17:13:41 -05:00
CaliBrain 0e2580030b Change APP_ENV from 'prod' to 'dev' default (#331)
Fix for #330
2025-12-08 14:48:33 -05:00
14 changed files with 65 additions and 62 deletions
+3 -4
View File
@@ -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=prod
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", "--"]
+20 -20
View File
@@ -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,10 @@ 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:
async_mode = 'threading'
# Socket.IO async mode.
# We run this app under Gunicorn with a gevent websocket worker (even when DEBUG=true),
# so Socket.IO should always use gevent here.
async_mode = 'gevent'
# Initialize Flask-SocketIO with reverse proxy support
socketio = SocketIO(
@@ -152,16 +149,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 +372,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 +383,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 +812,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,
-1
View File
@@ -9,7 +9,6 @@ services:
target: cwa-bd
environment:
DEBUG: true
APP_ENV: dev
USE_DOH: true
CUSTOM_DNS: cloudflare
volumes:
-1
View File
@@ -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)
+4 -2
View File
@@ -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
-1
View File
@@ -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
+8 -3
View File
@@ -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
+5 -3
View File
@@ -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
+6 -12
View File
@@ -105,17 +105,11 @@ 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
# 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
# Always run Gunicorn (even when DEBUG=true) to ensure Socket.IO WebSocket
# upgrades work reliably on customer machines.
# Map app LOG_LEVEL (often DEBUG/INFO/...) to gunicorn's --log-level (lowercase).
gunicorn_loglevel=$([ "$DEBUG" = "true" ] && echo debug || echo "${LOG_LEVEL:-info}" | tr '[:upper:]' '[:lower:]')
command="gunicorn --log-level ${gunicorn_loglevel} --access-logfile - --error-logfile - --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b ${FLASK_HOST:-0.0.0.0}:${FLASK_PORT:-8084} app:app"
# If DEBUG and not using an external bypass
if [ "$DEBUG" = "true" ] && [ "$USING_EXTERNAL_BYPASSER" != "true" ]; then
@@ -179,7 +173,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
+1 -6
View File
@@ -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"))
+5 -1
View File
@@ -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.
+3 -3
View File
@@ -490,9 +490,9 @@ function App() {
</main>
<Footer
buildVersion={config?.build_version || 'dev'}
releaseVersion={config?.release_version || 'dev'}
appEnv={config?.app_env || 'development'}
buildVersion={config?.build_version}
releaseVersion={config?.release_version}
debug={config?.debug}
/>
<ToastContainer toasts={toasts} />
+10 -4
View File
@@ -1,10 +1,15 @@
interface FooterProps {
buildVersion?: string;
releaseVersion?: string;
appEnv?: string;
debug?: boolean;
}
export const Footer = ({ buildVersion, releaseVersion, appEnv }: FooterProps) => {
export const Footer = ({ buildVersion, releaseVersion, debug }: FooterProps) => {
// Determine version display - show "dev" if no version is set
const versionDisplay = releaseVersion && releaseVersion !== 'N/A'
? releaseVersion
: 'dev';
return (
<footer
className="mt-8 border-t py-6"
@@ -17,8 +22,9 @@ export const Footer = ({ buildVersion, releaseVersion, appEnv }: FooterProps) =>
<div className="min-w-0 text-left">
<p className="text-sm opacity-80">Calibre Web Book Downloader</p>
<p className="text-xs opacity-60 mt-1">
Build: {buildVersion || 'dev'} • Release: {releaseVersion || 'dev'} • Env:{' '}
{appEnv || 'development'}
Version: {versionDisplay}
{buildVersion && buildVersion !== 'N/A' && ` (${buildVersion})`}
{debug && ' • Debug Mode'}
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
-1
View File
@@ -71,7 +71,6 @@ export interface Toast {
export interface AppConfig {
calibre_web_url: string;
debug: boolean;
app_env: string;
build_version: string;
release_version: string;
book_languages: Language[];