mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 03:17:15 +08:00
OPENAPI_CORS_ALLOW_ORIGINS env var defaults to empty (same-origin only). Operators expand for third-party integrations via comma-separated list. Allowed headers: Authorization, Content-Type, X-CSRF-Token. Methods: GET POST PATCH DELETE OPTIONS. Max-Age 600s. supports_credentials=True so cookie-authed approve/deny work once Phase D moves them in. Disallowed origins receive a normal 200 OPTIONS response without the Access-Control-Allow-Origin header — flask-cors's standard behavior; browser blocks the cross-origin request from the disallowed origin. Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
129 lines
5.1 KiB
Python
129 lines
5.1 KiB
Python
from configs import dify_config
|
|
from constants import HEADER_NAME_APP_CODE, HEADER_NAME_CSRF_TOKEN, HEADER_NAME_PASSPORT
|
|
from dify_app import DifyApp
|
|
|
|
BASE_CORS_HEADERS: tuple[str, ...] = ("Content-Type", HEADER_NAME_APP_CODE, HEADER_NAME_PASSPORT)
|
|
SERVICE_API_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, "Authorization")
|
|
AUTHENTICATED_HEADERS: tuple[str, ...] = (*SERVICE_API_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
|
FILES_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
|
EMBED_HEADERS: tuple[str, ...] = ("Content-Type", HEADER_NAME_APP_CODE)
|
|
EXPOSED_HEADERS: tuple[str, ...] = ("X-Version", "X-Env", "X-Trace-Id")
|
|
OPENAPI_HEADERS: tuple[str, ...] = ("Authorization", "Content-Type", HEADER_NAME_CSRF_TOKEN)
|
|
OPENAPI_MAX_AGE_SECONDS: int = 600
|
|
|
|
|
|
def _apply_cors_once(bp, /, **cors_kwargs):
|
|
"""Make CORS idempotent so blueprints can be reused across multiple app instances."""
|
|
|
|
if getattr(bp, "_dify_cors_applied", False):
|
|
return
|
|
|
|
from flask_cors import CORS
|
|
|
|
CORS(bp, **cors_kwargs)
|
|
bp._dify_cors_applied = True
|
|
|
|
|
|
def init_app(app: DifyApp):
|
|
# register blueprint routers
|
|
|
|
from controllers.console import bp as console_app_bp
|
|
from controllers.files import bp as files_bp
|
|
from controllers.inner_api import bp as inner_api_bp
|
|
from controllers.mcp import bp as mcp_bp
|
|
from controllers.openapi import bp as openapi_bp
|
|
from controllers.service_api import bp as service_api_bp
|
|
from controllers.trigger import bp as trigger_bp
|
|
from controllers.web import bp as web_bp
|
|
|
|
_apply_cors_once(
|
|
service_api_bp,
|
|
allow_headers=list(SERVICE_API_HEADERS),
|
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
)
|
|
app.register_blueprint(service_api_bp)
|
|
|
|
# User-scoped programmatic API. Default empty allowlist = same-origin
|
|
# only; expand via OPENAPI_CORS_ALLOW_ORIGINS for third-party
|
|
# integrations. supports_credentials so cookie-authed approve/deny
|
|
# work; cross-origin OPTIONS without an allowed origin will fail
|
|
# the same as on the console blueprint.
|
|
_apply_cors_once(
|
|
openapi_bp,
|
|
resources={r"/*": {"origins": dify_config.OPENAPI_CORS_ALLOW_ORIGINS}},
|
|
supports_credentials=True,
|
|
allow_headers=list(OPENAPI_HEADERS),
|
|
methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
max_age=OPENAPI_MAX_AGE_SECONDS,
|
|
)
|
|
app.register_blueprint(openapi_bp)
|
|
|
|
_apply_cors_once(
|
|
web_bp,
|
|
resources={
|
|
# Embedded bot endpoints (unauthenticated, cross-origin safe)
|
|
r"^/chat-messages$": {
|
|
"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS,
|
|
"supports_credentials": False,
|
|
"allow_headers": list(EMBED_HEADERS),
|
|
"methods": ["GET", "POST", "OPTIONS"],
|
|
},
|
|
r"^/chat-messages/.*": {
|
|
"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS,
|
|
"supports_credentials": False,
|
|
"allow_headers": list(EMBED_HEADERS),
|
|
"methods": ["GET", "POST", "OPTIONS"],
|
|
},
|
|
# Default web application endpoints (authenticated)
|
|
r"/*": {
|
|
"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS,
|
|
"supports_credentials": True,
|
|
"allow_headers": list(AUTHENTICATED_HEADERS),
|
|
"methods": ["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
|
},
|
|
},
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
)
|
|
app.register_blueprint(web_bp)
|
|
|
|
_apply_cors_once(
|
|
console_app_bp,
|
|
resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}},
|
|
supports_credentials=True,
|
|
allow_headers=list(AUTHENTICATED_HEADERS),
|
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
)
|
|
app.register_blueprint(console_app_bp)
|
|
|
|
_apply_cors_once(
|
|
files_bp,
|
|
allow_headers=list(FILES_HEADERS),
|
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
)
|
|
app.register_blueprint(files_bp)
|
|
|
|
app.register_blueprint(inner_api_bp)
|
|
app.register_blueprint(mcp_bp)
|
|
|
|
# SSO-branch device-flow routes. No CORS config — these endpoints are
|
|
# user-interactive (same-origin browser traffic) and cookie-authed;
|
|
# allowing cross-origin would defeat the SameSite=Lax cookie's purpose.
|
|
# Gated on ENABLE_OAUTH_BEARER: without the bearer authenticator, tokens
|
|
# minted here cannot be validated, so skip the blueprint entirely.
|
|
if dify_config.ENABLE_OAUTH_BEARER:
|
|
from controllers.oauth_device_sso import bp as oauth_device_sso_bp
|
|
app.register_blueprint(oauth_device_sso_bp)
|
|
|
|
# Register trigger blueprint with CORS for webhook calls
|
|
_apply_cors_once(
|
|
trigger_bp,
|
|
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
|
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH", "HEAD"],
|
|
expose_headers=list(EXPOSED_HEADERS),
|
|
)
|
|
app.register_blueprint(trigger_bp)
|