Completed
Push — master ( e7ae56...f640e6 )
by Nazar
04:35
created

Server::host_parse()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 13
nc 10
nop 1
dl 0
loc 16
ccs 13
cts 13
cp 1
crap 8
rs 7.7777
c 0
b 0
f 0
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 64
	public function init_server ($server = []) {
89 64
		$this->fill_headers($server);
90
		/**
91
		 * Add some defaults to avoid isset() hell afterwards
92
		 */
93
		$server += [
94 64
			'QUERY_STRING'    => '',
95
			'REMOTE_ADDR'     => '127.0.0.1',
96
			'REQUEST_URI'     => '/',
97
			'REQUEST_METHOD'  => 'GET',
98
			'SERVER_PROTOCOL' => 'HTTP/1.1'
99
		];
100 64
		$this->fill_server_properties($server);
101 64
	}
102
	/**
103
	 * @param string[] $server
104
	 */
105 64
	protected function fill_server_properties ($server) {
106 64
		$this->parse_forwarded_header();
107 64
		$this->cli          = @$server['CLI'] === true;
108 64
		$this->method       = strtoupper($server['REQUEST_METHOD']);
109 64
		$this->host         = $this->host($server);
110 64
		$this->secure       = $this->secure($server);
111 64
		$this->scheme       = $this->secure ? 'https' : 'http';
112 64
		$this->protocol     = $server['SERVER_PROTOCOL'];
113 64
		$this->query_string = $server['QUERY_STRING'];
114 64
		$this->uri          = null_byte_filter(rawurldecode($server['REQUEST_URI'])) ?: '/';
115 64
		if (strpos($this->uri, '/index.php') === 0) {
116 2
			$this->uri = substr($this->uri, 10);
117
		}
118 64
		$this->path        = explode('?', $this->uri, 2)[0];
119 64
		$this->remote_addr = $server['REMOTE_ADDR'];
120 64
		$this->ip          = $this->ip();
121 64
	}
122
	/**
123
	 * @param string[] $server
124
	 */
125 64
	protected function fill_headers ($server) {
126 64
		$headers = [];
127 64
		foreach ($server as $header => $header_content) {
128 64
			if (strpos($header, 'HTTP_') === 0) {
129 54
				$header = substr($header, 5);
130 64
			} elseif (strpos($header, 'CONTENT_') !== 0) {
131 64
				continue;
132
			}
133 62
			$header           = strtolower(str_replace('_', '-', $header));
134 62
			$headers[$header] = $header_content;
135
		}
136 64
		$this->headers = $headers;
137 64
	}
138
	/**
139
	 * Parse `Forwarded` header into `$this->forwarded`
140
	 */
141 64
	protected function parse_forwarded_header () {
142 64
		$this->forwarded = [];
143 64
		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 64
	}
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 64
	protected function ip () {
156
		$ips = [
157 64
			@$this->forwarded['for'],
158 64
			$this->header('x-forwarded-for'),
159 64
			$this->header('client-ip'),
160 64
			$this->header('x-forwarded'),
161 64
			$this->header('x-cluster-client-ip'),
162 64
			$this->header('forwarded-for')
163
		];
164 64
		$ips = array_filter($ips, 'trim');
165 64
		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 64
		return $this->remote_addr;
172
	}
173
	/**
174
	 * The best guessed host
175
	 *
176
	 * @param string[] $server
177
	 *
178
	 * @return string
179
	 */
180 64
	protected function host ($server) {
181 64
		list($host, $port) = $this->host_parse($server);
182 64
		if (preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host) !== '') {
183 2
			return '';
184
		}
185 64
		$expected_port = $this->secure ? 443 : 80;
186 64
		if ($port && $port != $expected_port) {
187 2
			$host .= ':'.(int)$port;
188
		}
189 64
		return $host;
190
	}
191
	/**
192
	 * @param string[] $server
193
	 *
194
	 * @return string[]
195
	 */
196 64
	protected function host_parse ($server) {
197 64
		$host                  = @$server['SERVER_NAME'] ?: '';
198 64
		$port                  = '';
199 64
		$forwarded_host_header = $this->header('x-forwarded-host');
200 64
		$host_header           = $this->header('host');
201 64
		if (!$host && $forwarded_host_header) {
202 2
			list($host, $port) = explode(':', $forwarded_host_header) + [1 => $this->header('x-forwarded-port')];
203 64
		} elseif ($host_header) {
204 54
			if (!$host || filter_var($host, FILTER_VALIDATE_IP)) {
205 2
				$host = $host_header;
206 54
			} elseif (strpos($host_header, ':') !== false) {
207 2
				$port = explode(':', $host_header)[1];
208
			}
209
		}
210 64
		return [$host, $port];
211
	}
212
	/**
213
	 * Secure protocol detection
214
	 *
215
	 * @param array $server
216
	 *
217
	 * @return bool
218
	 */
219 64
	protected function secure ($server) {
220 64
		return @$server['HTTPS'] ? $server['HTTPS'] !== 'off' : in_array(
221 64
			'https',
222
			[
223 64
				@$server['REQUEST_SCHEME'],
224 64
				@$this->forwarded['proto'],
225 64
				$this->header('x-forwarded-proto')
226
			]
227
		);
228
	}
229
	/**
230
	 * Get header by name
231
	 *
232
	 * @param string $name Case-insensitive
233
	 *
234
	 * @return string Header content if exists or empty string otherwise
235
	 */
236 82
	public function header ($name) {
237 82
		$name = strtolower($name);
238 82
		return isset($this->headers[$name]) ? $this->headers[$name] : '';
239
	}
240
}
241