Add CorsRegexEnvProcessor and corresponding tests

- 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.
This commit is contained in:
hamed
2026-07-07 14:58:41 +03:30
parent 4ee1524f31
commit 87f4d1695f
18 changed files with 2536 additions and 1343 deletions
@@ -0,0 +1,198 @@
# رفع دائمی 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 آن را خراب نمی‌کند. هدف:
1. یک **Env Var Processor** سفارشی (`cors_regex`) بساز که رشته‌ی کامادار `ALLOWED_FRONTEND_HOSTS`
را بگیرد و همان regex نهایی CORS را در PHP بسازد.
2. `nelmio_cors.yaml` را طوری تغییر بده که از `%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%` استفاده کند.
3. متغیر `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`:
```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:
```yaml
$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
<?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` بماند.
```yaml
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 ست نکند).
### ۴. تست
```bash
# ساخت 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`) را به‌روز کن.