mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 15:47:50 +09:00
115 lines
5.5 KiB
Python
115 lines
5.5 KiB
Python
"""Flask extensions, database connections, and request middleware."""
|
|
from flask_cors import CORS
|
|
|
|
def configure_application(app, db, migrate, jwt, limiter):
|
|
# Trust the reverse proxy's forwarding headers to derive the real client IP
|
|
# (config-gated; default off). ProxyFix rewrites request.remote_addr from the
|
|
# rightmost TRUSTED_PROXY_HOPS entries of X-Forwarded-For — the hops our own
|
|
# proxies appended — so a client-forged leftmost value is ignored. Applied
|
|
# before the limiter and request handlers so every remote_addr consumer
|
|
# (flask-limiter's get_remote_address, get_client_ip(), audit logs) benefits.
|
|
if app.config.get('TRUST_PROXY_HEADERS'):
|
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
hops = app.config.get('TRUSTED_PROXY_HOPS', 1)
|
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=hops, x_proto=1, x_host=0, x_port=0)
|
|
|
|
# Initialize extensions
|
|
db.init_app(app)
|
|
from app.middleware.request_profiling import register_request_profiling
|
|
register_request_profiling(app, db)
|
|
migrate.init_app(app, db)
|
|
jwt.init_app(app)
|
|
# Storage backend comes from app.config's RATELIMIT_STORAGE_URI when set
|
|
# (see config.py); unset = in-memory, correct for the single-worker design.
|
|
limiter.init_app(app)
|
|
|
|
# SQLite concurrency tuning: the queue-bus consumers, job system and
|
|
# metrics collector share one database file with request handling, and in
|
|
# the default journal mode any writer locks the WHOLE file — readers
|
|
# included — producing sporadic "database is locked" errors under load.
|
|
# WAL lets readers coexist with a writer, a generous busy timeout absorbs
|
|
# the remaining writer-writer contention, and synchronous=NORMAL is the
|
|
# safe pairing for WAL. (journal_mode persists in the database file; the
|
|
# per-connection pragmas re-assert it for fresh files.)
|
|
if app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite') and not app.config.get('TESTING'):
|
|
# (Skipped under TESTING: tests/conftest.py owns connection pragmas
|
|
# for the test process — journal_mode=MEMORY et al. — and two
|
|
# listeners fighting over journal_mode on the same file deadlocks.)
|
|
from sqlalchemy import event
|
|
|
|
with app.app_context():
|
|
@event.listens_for(db.engine, 'connect')
|
|
def _sqlite_tune(dbapi_connection, _record):
|
|
cursor = dbapi_connection.cursor()
|
|
cursor.execute('PRAGMA journal_mode=WAL')
|
|
cursor.execute('PRAGMA busy_timeout=30000')
|
|
cursor.execute('PRAGMA synchronous=NORMAL')
|
|
cursor.close()
|
|
|
|
# Build CORS origins. Start with static config/env, then append the
|
|
# persisted canonical domain from system settings so pointing an A record
|
|
# at the panel works without restarting to edit .env.
|
|
cors_origins = list(app.config['CORS_ORIGINS'])
|
|
try:
|
|
with app.app_context():
|
|
from app.services.settings_service import SettingsService
|
|
from app.utils.domain import canonical_origin
|
|
canonical_domain = SettingsService.get('canonical_domain', '') or ''
|
|
if canonical_domain:
|
|
https_enabled = SettingsService.get('canonical_https_enabled', False) or False
|
|
origin = canonical_origin(canonical_domain, https_enabled)
|
|
if origin not in cors_origins:
|
|
cors_origins.append(origin)
|
|
except Exception:
|
|
# Database may not exist yet during first install / migrations.
|
|
pass
|
|
|
|
CORS(
|
|
app,
|
|
origins=cors_origins,
|
|
supports_credentials=True,
|
|
allow_headers=[
|
|
'Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key',
|
|
'X-Request-ID',
|
|
],
|
|
expose_headers=['X-Request-ID'],
|
|
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
|
|
# Every panel request carries an Authorization header, which makes it a
|
|
# non-simple cross-origin request: without Access-Control-Max-Age the
|
|
# browser re-runs a preflight OPTIONS every few seconds (Chrome's
|
|
# default cache is 5s), so a cross-origin panel pays two round trips per
|
|
# call forever. One hour is the practical ceiling — Chrome caps the
|
|
# value at 2h, Firefox at 24h, and both take the smaller of the two.
|
|
max_age=3600,
|
|
)
|
|
|
|
# Assign one diagnostic ID at the outer API boundary. Downstream handlers,
|
|
# logs and jobs can reuse g.request_id without inventing independent IDs.
|
|
from app.middleware.request_id import register_request_id
|
|
register_request_id(app)
|
|
|
|
# Register security headers middleware
|
|
from app.middleware.security import register_security_headers
|
|
register_security_headers(app)
|
|
|
|
# Demo mode guard — config-gated (default off), blocks mutating API calls
|
|
from app.middleware.demo import init_demo_mode
|
|
init_demo_mode(app)
|
|
|
|
# Register API key authentication middleware
|
|
from app.middleware.api_key_auth import register_api_key_auth
|
|
register_api_key_auth(app)
|
|
|
|
# Register API analytics middleware
|
|
from app.middleware.api_analytics import register_api_analytics
|
|
register_api_analytics(app)
|
|
|
|
# Register fallback audit logging for authenticated mutating API requests
|
|
from app.middleware.audit import register_audit_fallback
|
|
register_audit_fallback(app)
|
|
|
|
# Update rate limiter with custom key function
|
|
from app.middleware.rate_limit import get_rate_limit_key, register_rate_limit_headers
|
|
limiter._key_func = get_rate_limit_key
|
|
register_rate_limit_headers(app)
|