- Implemented CorsRegexEnvProcessor to build CORS origin regex from a comma-separated host list (ALLOWED_FRONTEND_HOSTS). - Added tests for CorsRegexEnvProcessor to validate regex generation and matching behavior. - Created JSON files for AST representation of the new classes and tests.
11 KiB
رفع دائمی CORS در Coolify: ساخت regex از ALLOWED_FRONTEND_HOSTS (حذف وابستگی به CORS_ALLOW_ORIGIN)
پروژه
clinicpro (Backend — CORS/nelmio)
زمینه
فرانتاند عمومی (nobat724_front) روی دامنههای شهری (مثل yasuj-nobat.ir) هنگام
POST /api/v1/user/send-code این خطا را میگیرد:
Access to XMLHttpRequest at 'https://clinic-pro.ir/api/v1/user/send-code'
from origin 'https://yasuj-nobat.ir' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
تشخیص قطعی (تست شده): کد و کانفیگ nelmio سالم است — روی محیط لوکال ddev همان regex،
هدر access-control-allow-origin را درست برمیگرداند. اما روی پروداکشن، پاسخ preflight هیچ
هدر access-control-allow-origin ندارد. علت: Coolify مقدار env متغیر CORS_ALLOW_ORIGIN را
خراب میکند، چون آن مقدار یک regex است که با $ تمام میشود و شامل \ است؛ Coolify هنگام
interpolation، $ و backslashها را دستکاری/escape میکند و در نهایت regex خالی/نادرست به
کانتینر میرسد → nelmio هیچ origin ای را match نمیکند → هدر ACAO ست نمیشود.
راهحل رسمی Coolify («Is Literal?») خودش باگهای گزارششده دارد (مقدار را داخل '...' میپیچد یا
دوباره escape میکند). بنابراین بهجای اتکا به یک env شکنندهی حاوی $/\, باید regex را
سمت برنامه از روی یک env امن بسازیم.
مشکل / هدف
env امنِ ALLOWED_FRONTEND_HOSTS از قبل وجود دارد و فقط شامل نام دامنهها با کاما است
(بدون $، بدون \, بدون کاراکتر خاص) → Coolify آن را خراب نمیکند. هدف:
- یک Env Var Processor سفارشی (
cors_regex) بساز که رشتهی کامادارALLOWED_FRONTEND_HOSTSرا بگیرد و همان regex نهایی CORS را در PHP بسازد. nelmio_cors.yamlرا طوری تغییر بده که از%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%استفاده کند.- متغیر
CORS_ALLOW_ORIGINرا از فایلهای نمونهی env حذف/deprecate کن تا دیگر لازم نباشد در Coolify ست شود (منبع باگ حذف میشود).
نتیجه: تنها env مربوط به CORS یک لیست سادهٔ کامادارِ Coolify-safe است.
فایلهای مرتبط
| فایل | نقش |
|---|---|
config/packages/nelmio_cors.yaml |
فعلاً allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] با origin_regex: true |
config/services.yaml |
اینجا %env(ALLOWED_FRONTEND_HOSTS)% به کنترلرها bind شده (خط ~۸۹) |
src/Shared/... (جدید) |
کلاس Env Var Processor سفارشی |
.env |
مقدار پیشفرض CORS_ALLOW_ORIGIN و ALLOWED_FRONTEND_HOSTS |
.env.coolify.example / .env.liara.example |
نمونهی env برای دیپلوی — باید بهروز شوند |
docker/gen-cors-env.php |
تولیدکنندهی خروجی CORS از docker/frontend-domains.json (بررسی همخوانی) |
وضعیت فعلی
config/packages/nelmio_cors.yaml:
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'Content-Disposition']
expose_headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset']
max_age: 3600
allow_credentials: false
paths:
'^/api/': { allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] }
'^/oauth/': { allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] }
'^/health': { allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] }
config/services.yaml (خط ~۸۹) — الگوی bind فعلی env:
$allowedFrontendHosts: '%env(ALLOWED_FRONTEND_HOSTS)%'
.env (مقدار پیشفرض فعلی که در پروداکشن خراب میشود):
CORS_ALLOW_ORIGIN='^https?://([a-z0-9-]+\.)*(clinic-pro\.ddev\.site|localhost|127\.0\.0\.1|[a-z0-9-]+-nobat\.ir|nobat724\.com)(:[0-9]+)?$'
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,...,zanjan-nobat.ir
وظایف
۱. ساخت Env Var Processor سفارشی cors_regex
یک کلاس بساز (مثلاً src/Shared/DependencyInjection/CorsRegexEnvProcessor.php) که
Symfony\Component\DependencyInjection\EnvVarProcessorInterface را پیاده کند. ورودی یک نام env
(ALLOWED_FRONTEND_HOSTS) است؛ خروجی یک regex معتبر PCRE برای nelmio.
<?php
namespace App\Shared\DependencyInjection;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
final class CorsRegexEnvProcessor implements EnvVarProcessorInterface
{
public function getEnv(string $prefix, string $name, \Closure $getEnv): string
{
$raw = (string) $getEnv($name); // "ahvaz-nobat.ir,arak-nobat.ir,..."
$hosts = array_filter(array_map('trim', explode(',', $raw)));
if ($hosts === []) {
// fail-safe: هیچ origin ای مجاز نیست (بهتر از regex خالیِ نامعتبر)
return '^https://$a';
}
$alts = implode('|', array_map(
static fn (string $h) => preg_quote($h, '#'),
$hosts
));
// با یا بدون زیردامنه، فقط https، انکورشده. بدون نیاز به هیچ env حاوی $.
return '^https://([a-z0-9-]+\.)*(' . $alts . ')$';
}
public static function getProvidedTypes(): array
{
return ['cors_regex' => 'string'];
}
}
اگر پورت برای محیط لوکال لازم است (
localhost:3000)، الگو را طوری بساز که پورت اختیاری را هم بپذیرد؛ اما چونALLOWED_FRONTEND_HOSTSفقط دامنههای پروداکشن است، برای dev میتوانlocalhostرا هم در همان لیست env محیط dev گذاشت یا در processor بهصورت شرطی افزود. تصمیم را در پیادهسازی صریح بگیر و ساده نگهدار.
اطمینان حاصل کن کلاس بهعنوان env processor شناخته میشود: چون EnvVarProcessorInterface را
پیاده میکند و autoconfigure روشن است، Symfony آن را خودکار با تگ
container.env_var_processor ثبت میکند. اگر autowire/autoconfigure برای این namespace فعال
نیست، در config/services.yaml صریح ثبتش کن.
۲. تغییر nelmio_cors.yaml برای استفاده از processor
همهی %env(CORS_ALLOW_ORIGIN)% را با %env(cors_regex:ALLOWED_FRONTEND_HOSTS)% جایگزین کن.
origin_regex: true بماند.
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
# ... بقیه بدون تغییر ...
paths:
'^/api/': { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
'^/oauth/': { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
'^/health': { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
۳. بهروزرسانی فایلهای env
.env: میتوانیCORS_ALLOW_ORIGINرا نگه داری اما دیگر مصرف نمیشود؛ بهتر است حذف/کامنت شود تا گمراهکننده نباشد.ALLOWED_FRONTEND_HOSTSباید شامل دامنههای dev هم باشد اگر لازم است (clinic-pro.ddev.site,localhostو ...) — تصمیم بگیر و مستند کن..env.coolify.exampleو.env.liara.example: بخش CORS را بازنویسی کن — توضیح بده که فقطALLOWED_FRONTEND_HOSTS(لیست کامادار، بدون کاراکتر خاص، Coolify-safe) لازم است وCORS_ALLOW_ORIGINدیگر استفاده نمیشود (تا کاربر آن را در Coolify ست نکند).
۴. تست
# ساخت regex درست است؟ (کانفیگ نهایی nelmio را چاپ کن)
ddev exec php bin/console cache:clear
ddev exec php bin/console debug:config nelmio_cors 2>&1 | grep -i allow_origin
# preflight لوکال باید ACAO بدهد:
curl -sk -i -X OPTIONS "https://clinic-pro.ddev.site/api/v1/user/send-code" \
-H "Origin: https://yasuj-nobat.ir" -H "Access-Control-Request-Method: POST" \
| grep -i "access-control-allow-origin"
# origin نامعتبر نباید ACAO بگیرد:
curl -sk -i -X OPTIONS "https://clinic-pro.ddev.site/api/v1/user/send-code" \
-H "Origin: https://evil.com" -H "Access-Control-Request-Method: POST" \
| grep -i "access-control-allow-origin" # باید خالی باشد
هر دو حالت باید درست کار کنند (origin مجاز ACAO بگیرد، نامجاز نگیرد).
نکات مهم
- هیچ env حاوی
$یا\نباید برای CORS لازم باشد — کل هدف همین است؛ALLOWED_FRONTEND_HOSTSفقط دامنههای کامادار است و Coolify آن را دستنخورده نگه میدارد. preg_quote($h, '#')برای escape نقطهها ضروری است (چون nelmio با delimiter#regex را کامپایل میکند — الگو را با همان delimiter سازگار نگهدار؛ اگر nelmio delimiter دیگری میگذارد، با تستdebug:configو preflight تأیید کن).- fail-safe: اگر لیست خالی بود، یک regex بساز که هیچچیز را match نکند (نه یک regex خالی که ممکن است بهطور ناخواسته همه را رد یا قبول کند).
- deploy: بعد از merge، در Coolify فقط
ALLOWED_FRONTEND_HOSTSرا نگه دار وCORS_ALLOW_ORIGINرا حذف کن؛ سپس redeploy. باprintenv ALLOWED_FRONTEND_HOSTSداخل کانتینر صحت مقدار را چک کن. - این تغییر فقط کانفیگ/DI است؛ Entity/migration ندارد. مستندات API عوض نمیشود (رفتار endpointها
ثابت است)، اما اگر جایی CORS مستند شده، اشاره به منبع جدید (
ALLOWED_FRONTEND_HOSTS) را بهروز کن.