Passed
Push — main ( 6a18bd...fd1e99 )
by Tan
03:22
created

CorsMiddleware::getAllowedOrigin()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 27
rs 9.6111
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
0 ignored issues
show
introduced by
$customDomain is of type PhpOption\T, thus it always evaluated to true.
Loading history...
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
}
177