Completed
Push — master ( 27130a...a2c8d4 )
by Nazar
04:45
created

Server::parse_forwarded_header()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 9
rs 9.6666
ccs 7
cts 7
cp 1
crap 2
1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Request;
9
10
trait Server {
11
	/**
12
	 * Uppercase method, GET by default
13
	 *
14
	 * @var string
15
	 */
16
	public $method;
17
	/**
18
	 * The best guessed host
19
	 *
20
	 * @var string
21
	 */
22
	public $host;
23
	/**
24
	 * Schema `http` or `https`
25
	 *
26
	 * @var string
27
	 */
28
	public $scheme;
29
	/**
30
	 * Is requested with HTTPS
31
	 *
32
	 * @var bool
33
	 */
34
	public $secure;
35
	/**
36
	 * Protocol, for instance: `HTTP/1.0`, `HTTP/1.1` (default), HTTP/2.0
37
	 *
38
	 * @var string
39
	 */
40
	public $protocol;
41
	/**
42
	 * Path
43
	 *
44
	 * @var string
45
	 */
46
	public $path;
47
	/**
48
	 * URI, basically `$path?$query_string` (without `?` is query string is empty), `/` by default
49
	 *
50
	 * @var string
51
	 */
52
	public $uri;
53
	/**
54
	 * Query string
55
	 *
56
	 * @var string
57
	 */
58
	public $query_string;
59
	/**
60
	 * Where request came from, not necessary real IP of client, `127.0.0.1` by default
61
	 *
62
	 * @var string
63
	 */
64
	public $remote_addr;
65
	/**
66
	 * The best guessed IP of client (based on all known headers), `$this->remote_addr` by default
67
	 *
68
	 * @var string
69
	 */
70
	public $ip;
71
	/**
72
	 * Headers are normalized to lowercase keys with hyphen as separator, for instance: `connection`, `referer`, `content-type`, `accept-language`
73
	 *
74
	 * @var string[]
75
	 */
76
	public $headers;
77
	/**
78
	 * @var bool
79
	 */
80
	protected $cli;
81
	/**
82
	 * @var string[]
83
	 */
84
	public $forwarded;
85
	/**
86
	 * @param string[] $server Typically `$_SERVER`
87
	 */
88 44
	function init_server ($server = []) {
89 44
		$this->fill_headers($server);
90
		/**
91
		 * Add some defaults to avoid isset() hell afterwards
92
		 */
93
		$server += [
94 44
			'QUERY_STRING'    => '',
95
			'REMOTE_ADDR'     => '127.0.0.1',
96
			'REQUEST_URI'     => '/',
97
			'REQUEST_METHOD'  => 'GET',
98
			'SERVER_PROTOCOL' => 'HTTP/1.1'
99
		];
100 44
		$this->fill_server_properties($server);
101 44
	}
102
	/**
103
	 * @param string[] $server
104
	 */
105 44
	protected function fill_server_properties ($server) {
106 44
		$this->parse_forwarded_header();
107 44
		$this->cli          = @$server['CLI'] === true;
108 44
		$this->method       = strtoupper($server['REQUEST_METHOD']);
109 44
		$this->host         = $this->host($server);
110 44
		$this->secure       = $this->secure($server);
111 44
		$this->scheme       = $this->secure ? 'https' : 'http';
112 44
		$this->protocol     = $server['SERVER_PROTOCOL'];
113 44
		$this->query_string = $server['QUERY_STRING'];
114 44
		$this->uri          = null_byte_filter(urldecode($server['REQUEST_URI'])) ?: '/';
115 44
		if (strpos($this->uri, '/index.php') === 0) {
116 2
			$this->uri = substr($this->uri, 10);
117
		}
118 44
		$this->path        = explode('?', $this->uri, 2)[0];
119 44
		$this->remote_addr = $server['REMOTE_ADDR'];
120 44
		$this->ip          = $this->ip();
121 44
	}
122
	/**
123
	 * @param string[] $server
124
	 */
125 44
	protected function fill_headers ($server) {
126 44
		$headers = [];
127 44
		foreach ($server as $header => $header_content) {
128 44
			if (strpos($header, 'HTTP_') === 0) {
129 36
				$header = substr($header, 5);
130 44
			} elseif (strpos($header, 'CONTENT_') !== 0) {
131 44
				continue;
132
			}
133 44
			$header           = strtolower(str_replace('_', '-', $header));
134 44
			$headers[$header] = $header_content;
135
		}
136 44
		$this->headers = $headers;
137 44
	}
138
	/**
139
	 * Parse `Forwarded` header into `$this->forwarded`
140
	 */
141 44
	protected function parse_forwarded_header () {
142 44
		$this->forwarded = [];
143 44
		if (preg_match_all('/(for|proto|by)=(.*)(?:;|$)/Ui', explode(',', $this->header('forwarded'))[0], $matches)) {
144 2
			$this->forwarded = array_combine(
145 2
				$matches[1],
146 2
				_trim($matches[2], " \t\n\r\0\x0B\"")
147
			);
148
		}
149 44
	}
150
	/**
151
	 * The best guessed IP of client (based on all known headers), `127.0.0.1` by default
152
	 *
153
	 * @return string
154
	 */
155 44
	protected function ip () {
156
		$ips = [
157 44
			@$this->forwarded['for'],
158 44
			$this->header('x-forwarded-for'),
159 44
			$this->header('client-ip'),
160 44
			$this->header('x-forwarded'),
161 44
			$this->header('x-cluster-client-ip'),
162 44
			$this->header('forwarded-for')
163
		];
164 44
		$ips = array_filter($ips, 'trim');
165 44
		foreach ($ips as $ip) {
166 2
			$ip = trim(explode(',', $ip)[0]);
167 2
			if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
168 2
				return $ip;
169
			}
170
		}
171 44
		return $this->remote_addr;
172
	}
173
	/**
174
	 * The best guessed host
175
	 *
176
	 * @param string[] $server
177
	 *
178
	 * @return string
179
	 */
180 44
	protected function host ($server) {
181 44
		$host                  = @$server['SERVER_NAME'] ?: '';
182 44
		$port                  = '';
183 44
		$expected_port         = $this->secure ? 443 : 80;
184 44
		$forwarded_host_header = $this->header('x-forwarded-host');
185 44
		$host_header           = $this->header('host');
186 44
		if (!$host && $forwarded_host_header) {
187 2
			list($host, $port) = explode(':', $forwarded_host_header) + [1 => $this->header('x-forwarded-port')];
188 44
		} elseif ($host_header) {
189 36
			if (!$host || filter_var($host, FILTER_VALIDATE_IP)) {
190 2
				$host = $host_header;
191 36
			} elseif (strpos($host_header, ':') !== false) {
192 2
				$port = explode(':', $host_header)[1];
193
			}
194
		}
195 44
		if ($port == $expected_port) {
196 2
			$port = '';
197
		}
198 44
		if (preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host) !== '') {
199 2
			return '';
200
		}
201 44
		return $host.($port ? ':'.(int)$port : '');
202
	}
203
	/**
204
	 * Secure protocol detection
205
	 *
206
	 * @param array $server
207
	 *
208
	 * @return bool
209
	 */
210 44
	protected function secure ($server) {
211 44
		return @$server['HTTPS'] ? $server['HTTPS'] !== 'off' : in_array(
212 44
			'https',
213
			[
214 44
				@$server['REQUEST_SCHEME'],
215 44
				@$this->forwarded['proto'],
216 44
				$this->header('x-forwarded-proto')
217
			]
218
		);
219
	}
220
	/**
221
	 * Get header by name
222
	 *
223
	 * @param string $name Case-insensitive
224
	 *
225
	 * @return string Header content if exists or empty string otherwise
226
	 */
227 60
	function header ($name) {
228 60
		$name = strtolower($name);
229 60
		return isset($this->headers[$name]) ? $this->headers[$name] : '';
230
	}
231
}
232