- 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.
199 lines
11 KiB
Markdown
199 lines
11 KiB
Markdown
# رفع دائمی 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`) را بهروز کن.
|