Skip to content

Commit fd1e995

Browse files
committed
feat: enhance CORS middleware and update domain settings in configuration
1 parent 6a18bd2 commit fd1e995

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

config/cors.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
return [
4+
5+
/*
6+
|--------------------------------------------------------------------------
7+
| Cross-Origin Resource Sharing (CORS) Configuration
8+
|--------------------------------------------------------------------------
9+
|
10+
| Here you may configure your settings for cross-origin resource sharing
11+
| or "CORS". This determines what cross-origin operations may execute
12+
| in web browsers. You are free to adjust these settings as needed.
13+
|
14+
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
15+
|
16+
*/
17+
18+
'paths' => [
19+
'api/*',
20+
'sanctum/csrf-cookie',
21+
env('BLOG_API_ROUTE_PREFIX', 'cs-api') . '/*',
22+
env('BLOG_API_ROUTE_PREFIX', 'cs-api') . '/sanctum/csrf-cookie',
23+
],
24+
25+
'allowed_methods' => ['*'],
26+
27+
'allowed_origins' => [
28+
// Environment-specific URLs from .env
29+
env('BLOG_FE_URL'),
30+
env('BLOG_ADMIN_URL'),
31+
env('BLOG_API_URL'),
32+
env('APP_URL'),
33+
34+
// Development origins
35+
'http://localhost',
36+
'http://localhost:3000',
37+
'http://localhost:5173', // Vite default
38+
'http://127.0.0.1',
39+
'http://127.0.0.1:3000',
40+
41+
// Development with .local domains
42+
'http://cslant.com.local',
43+
'http://cslant.com.local:81',
44+
45+
// Production domains (without .local)
46+
'https://cslant.com',
47+
'https://api-docs.cslant.com',
48+
49+
// Staging domains
50+
'https://staging.cslant.com',
51+
],
52+
53+
'allowed_origins_patterns' => [
54+
// Development patterns (.local domains)
55+
'/^https?:\/\/[a-zA-Z0-9\-]+\.cslant\.com\.local(:\d+)?$/',
56+
'/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com\.local(:\d+)?$/',
57+
58+
// Production patterns (without .local)
59+
'/^https:\/\/[a-zA-Z0-9\-]+\.cslant\.com$/',
60+
'/^https:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com$/',
61+
'/^http:\/\/[a-zA-Z0-9\-]+\.cslant\.com$/',
62+
'/^http:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com$/',
63+
64+
// Staging patterns
65+
'/^https?:\/\/[a-zA-Z0-9\-]+\.staging\.cslant\.com$/',
66+
'/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.staging\.cslant\.com$/',
67+
68+
// Localhost patterns with any port
69+
'/^https?:\/\/localhost(:\d+)?$/',
70+
'/^https?:\/\/127\.0\.0\.1(:\d+)?$/',
71+
'/^https?:\/\/0\.0\.0\.0(:\d+)?$/',
72+
],
73+
74+
'allowed_headers' => [
75+
'Accept',
76+
'Authorization',
77+
'Content-Type',
78+
'X-Requested-With',
79+
'X-CSRF-TOKEN',
80+
'X-XSRF-TOKEN',
81+
'Origin',
82+
'Cache-Control',
83+
'Pragma',
84+
'X-Forwarded-For',
85+
'X-Forwarded-Proto',
86+
'X-Forwarded-Host',
87+
],
88+
89+
'exposed_headers' => [
90+
'Cache-Control',
91+
'Content-Language',
92+
'Content-Type',
93+
'Expires',
94+
'Last-Modified',
95+
'Pragma',
96+
],
97+
98+
'max_age' => 86400, // 24 hours
99+
100+
'supports_credentials' => true,
101+
];
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace CSlant\Blog\Api\Http\Middlewares;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class CorsMiddleware
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15+
*/
16+
public function handle(Request $request, Closure $next): Response
17+
{
18+
// Handle preflight OPTIONS request
19+
if ($request->getMethod() === 'OPTIONS') {
20+
return response('', 200)
21+
->header('Access-Control-Allow-Origin', $this->getAllowedOrigin($request))
22+
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH')
23+
->header('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With, X-CSRF-TOKEN, X-XSRF-TOKEN, Origin, Cache-Control, Pragma')
24+
->header('Access-Control-Allow-Credentials', 'true')
25+
->header('Access-Control-Max-Age', '86400');
26+
}
27+
28+
$response = $next($request);
29+
30+
// Add CORS headers to response
31+
$response->headers->set('Access-Control-Allow-Origin', $this->getAllowedOrigin($request));
32+
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
33+
$response->headers->set('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With, X-CSRF-TOKEN, X-XSRF-TOKEN, Origin, Cache-Control, Pragma');
34+
$response->headers->set('Access-Control-Allow-Credentials', 'true');
35+
$response->headers->set('Access-Control-Expose-Headers', 'Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma');
36+
37+
return $response;
38+
}
39+
40+
/**
41+
* Get allowed origin for the request.
42+
*/
43+
private function getAllowedOrigin(Request $request): string
44+
{
45+
$origin = $request->headers->get('Origin');
46+
47+
// Get environment-specific configurations
48+
$allowedOrigins = $this->getAllowedOrigins();
49+
50+
// Check if origin is in allowed list
51+
if (in_array($origin, $allowedOrigins)) {
52+
return $origin;
53+
}
54+
55+
// Check against patterns
56+
$allowedPatterns = $this->getAllowedPatterns();
57+
58+
foreach ($allowedPatterns as $pattern) {
59+
if (preg_match($pattern, $origin)) {
60+
return $origin;
61+
}
62+
}
63+
64+
// Default to first allowed origin or the origin itself if it matches basic security rules
65+
if ($this->isSecureOrigin($origin)) {
66+
return $origin;
67+
}
68+
69+
return $allowedOrigins[0] ?? '*';
70+
}
71+
72+
/**
73+
* Get all allowed origins based on environment.
74+
*/
75+
private function getAllowedOrigins(): array
76+
{
77+
$baseOrigins = [
78+
// Environment-specific URLs from .env
79+
env('BLOG_FE_URL'),
80+
env('BLOG_ADMIN_URL'),
81+
env('BLOG_API_URL'),
82+
env('APP_URL'),
83+
84+
// Development origins
85+
'http://localhost',
86+
'http://localhost:3000',
87+
'http://localhost:5173', // Vite default
88+
'http://127.0.0.1',
89+
'http://127.0.0.1:3000',
90+
91+
// Development with .local domains
92+
'http://cslant.com.local',
93+
'http://cslant.com.local:81',
94+
95+
// Production domains (without .local)
96+
'https://cslant.com',
97+
'https://api-docs.cslant.com',
98+
99+
// Staging domains
100+
'https://staging.cslant.com',
101+
];
102+
103+
// Filter out null values and return unique origins
104+
return array_unique(array_filter($baseOrigins));
105+
}
106+
107+
/**
108+
* Get allowed patterns for dynamic origin matching.
109+
*/
110+
private function getAllowedPatterns(): array
111+
{
112+
return [
113+
// Development patterns (.local domains)
114+
'/^https?:\/\/[a-zA-Z0-9\-]+\.cslant\.com\.local(:\d+)?$/',
115+
'/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com\.local(:\d+)?$/',
116+
117+
// Production patterns (without .local)
118+
'/^https:\/\/[a-zA-Z0-9\-]+\.cslant\.com$/',
119+
'/^https:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com$/',
120+
'/^http:\/\/[a-zA-Z0-9\-]+\.cslant\.com$/',
121+
'/^http:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.cslant\.com$/',
122+
123+
// Staging patterns
124+
'/^https?:\/\/[a-zA-Z0-9\-]+\.staging\.cslant\.com$/',
125+
'/^https?:\/\/[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.staging\.cslant\.com$/',
126+
127+
// Localhost patterns with any port
128+
'/^https?:\/\/localhost(:\d+)?$/',
129+
'/^https?:\/\/127\.0\.0\.1(:\d+)?$/',
130+
'/^https?:\/\/0\.0\.0\.0(:\d+)?$/',
131+
132+
// Custom domain patterns from environment
133+
$this->getCustomDomainPattern(),
134+
];
135+
}
136+
137+
/**
138+
* Get custom domain pattern from environment.
139+
*/
140+
private function getCustomDomainPattern(): string
141+
{
142+
$customDomain = env('CORS_CUSTOM_DOMAIN_PATTERN');
143+
return $customDomain ?: '/^$/'; // Empty pattern if not set
144+
}
145+
146+
/**
147+
* Check if origin is secure (basic security validation).
148+
*/
149+
private function isSecureOrigin(?string $origin): bool
150+
{
151+
// Allow null origin (for mobile apps, Postman, etc.)
152+
if (empty($origin)) {
153+
return true;
154+
}
155+
156+
// Must be a valid URL
157+
if (!filter_var($origin, FILTER_VALIDATE_URL)) {
158+
return false;
159+
}
160+
161+
$parsed = parse_url($origin);
162+
163+
// Must have valid scheme
164+
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
165+
return false;
166+
}
167+
168+
// Block dangerous hosts
169+
$dangerousHosts = ['0.0.0.0', '255.255.255.255'];
170+
if (in_array($parsed['host'] ?? '', $dangerousHosts)) {
171+
return false;
172+
}
173+
174+
return true;
175+
}
176+
}

0 commit comments

Comments
 (0)