Plugin Directory

source: jetpack/trunk/jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php @ 3068647

Last change on this file since 3068647 was 3068647, checked in by zinigor, 3 months ago

Updating trunk to version 13.3.1

File size: 10.5 KB
Line 
1<?php
2/**
3 * HTTP request representation specific for the WAF.
4 *
5 * @package automattic/jetpack-waf
6 */
7
8namespace Automattic\Jetpack\Waf;
9
10require_once __DIR__ . '/functions.php';
11
12/**
13 * Request representation.
14 *
15 * @template RequestFile as array{ name: string, filename: string }
16 */
17class Waf_Request {
18        /**
19         * The request URL, broken into three pieces: the host, the filename, and the query string
20         *
21         * @example for `https://wordpress.com/index.php?myvar=red`
22         *          $this->url = [ 'https://wordpress.com', '/index.php', '?myvar=red' ]
23         * @var array{ 0: string, 1: string, 2: string }|null
24         */
25        protected $url = null;
26
27        /**
28         * Trusted proxies.
29         *
30         * @var array List of trusted proxy IP addresses.
31         */
32        private $trusted_proxies = array();
33
34        /**
35         * Trusted headers.
36         *
37         * @var array List of headers to trust from the trusted proxies.
38         */
39        private $trusted_headers = array();
40
41        /**
42         * Sets the list of IP addresses for the proxies to trust. Trusted headers will only be accepted as the
43         * user IP address from these IP adresses.
44         *
45         * Popular choices include:
46         * - 192.168.0.1
47         * - 10.0.0.1
48         *
49         * @param array $proxies List of proxy IP addresses.
50         * @return void
51         */
52        public function set_trusted_proxies( $proxies ) {
53                $this->trusted_proxies = (array) $proxies;
54        }
55
56        /**
57         * Sets the list of headers to be trusted from the proxies. These headers will only be taken into account
58         * if the request comes from a trusted proxy as configured with set_trusted_proxies().
59         *
60         * Popular choices include:
61         * - HTTP_CLIENT_IP
62         * - HTTP_X_FORWARDED_FOR
63         * - HTTP_X_FORWARDED
64         * - HTTP_X_CLUSTER_CLIENT_IP
65         * - HTTP_FORWARDED_FOR
66         * - HTTP_FORWARDED
67         *
68         * @param array $headers List of HTTP header strings.
69         * @return void
70         */
71        public function set_trusted_headers( $headers ) {
72                $this->trusted_headers = (array) $headers;
73        }
74
75        /**
76         * Determines the users real IP address based on the settings passed to set_trusted_proxies() and
77         * set_trusted_headers() before. On CLI, this will be null.
78         *
79         * @return string|null
80         */
81        public function get_real_user_ip_address() {
82                $remote_addr = ! empty( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
83
84                if ( in_array( $remote_addr, $this->trusted_proxies, true ) ) {
85                        $ip_by_header = $this->get_ip_by_header( array_merge( $this->trusted_headers, array( 'REMOTE_ADDR' ) ) );
86                        if ( ! empty( $ip_by_header ) ) {
87                                return $ip_by_header;
88                        }
89                }
90
91                return $remote_addr;
92        }
93
94        /**
95         * Iterates through a given list of HTTP headers and attempts to get the IP address from the header that
96         * a proxy sends along. Make sure you trust the IP address before calling this method.
97         *
98         * @param array $headers The list of headers to check.
99         * @return string|null
100         */
101        private function get_ip_by_header( $headers ) {
102                foreach ( $headers as $key ) {
103                        if ( isset( $_SERVER[ $key ] ) ) {
104                                foreach ( explode( ',', wp_unslash( $_SERVER[ $key ] ) ) as $ip ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- filter_var is applied below.
105                                        $ip = trim( $ip );
106
107                                        if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
108                                                return $ip;
109                                        }
110                                }
111                        }
112                }
113
114                return null;
115        }
116
117        /**
118         * Returns the headers that were sent with this request
119         *
120         * @return array{ 0: string, 1: scalar }[]
121         */
122        public function get_headers() {
123                $value              = array();
124                $has_content_type   = false;
125                $has_content_length = false;
126                foreach ( $_SERVER as $k => $v ) {
127                        $k = strtolower( $k );
128                        if ( 'http_' === substr( $k, 0, 5 ) ) {
129                                $value[] = array( $this->normalize_header_name( substr( $k, 5 ) ), $v );
130                        } elseif ( 'content_type' === $k && '' !== $v ) {
131                                $has_content_type = true;
132                                $value[]          = array( 'content-type', $v );
133                        } elseif ( 'content_length' === $k && '' !== $v ) {
134                                $has_content_length = true;
135                                $value[]            = array( 'content-length', $v );
136                        }
137                }
138                if ( ! $has_content_type ) {
139                        // default Content-Type per RFC 7231 section 3.1.5.5.
140                        $value[] = array( 'content-type', 'application/octet-stream' );
141                }
142                if ( ! $has_content_length ) {
143                        $value[] = array( 'content-length', '0' );
144                }
145
146                return $value;
147        }
148
149        /**
150         * Returns the value of a specific header that was sent with this request
151         *
152         * @param string $name The name of the header to retrieve.
153         * @return string
154         */
155        public function get_header( $name ) {
156                $name = $this->normalize_header_name( $name );
157                foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
158                        if ( $header_name === $name ) {
159                                return $header_value;
160                        }
161                }
162                return '';
163        }
164
165        /**
166         * Change a header name to all-lowercase and replace spaces and underscores with dashes.
167         *
168         * @param string $name The header name to normalize.
169         * @return string
170         */
171        public function normalize_header_name( $name ) {
172                return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
173        }
174
175        /**
176         * Get the method for this request (GET, POST, etc).
177         *
178         * @return string
179         */
180        public function get_method() {
181                return isset( $_SERVER['REQUEST_METHOD'] )
182                        ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ), FILTER_DEFAULT )
183                        : '';
184        }
185
186        /**
187         * Get the protocol for this request (HTTP, HTTPS, etc)
188         *
189         * @return string
190         */
191        public function get_protocol() {
192                return isset( $_SERVER['SERVER_PROTOCOL'] )
193                        ? filter_var( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ), FILTER_DEFAULT )
194                        : '';
195        }
196
197        /**
198         * Returns the URL parts for this request.
199         *
200         * @see $this->url
201         * @return array{ 0: string, 1: string, 2: string }
202         */
203        protected function get_url() {
204                if ( null !== $this->url ) {
205                        return $this->url;
206                }
207
208                $uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
209                if ( false !== strpos( $uri, '?' ) ) {
210                        // remove the query string (we'll pull it from elsewhere later)
211                        $uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
212                } else {
213                        $uri = urldecode( $uri );
214                }
215                $query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
216                if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
217                        // sometimes $_SERVER[REQUEST_URI] already includes the full domain name
218                        $uri_host  = substr( $uri, 0, strpos( $uri, '/', 8 ) );
219                        $uri_path  = substr( $uri, strlen( $uri_host ) );
220                        $this->url = array( $uri_host, $uri_path, $query_string );
221                } else {
222                        // otherwise build the URI manually
223                        $uri_scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] )
224                                ? 'https'
225                                : 'http';
226                        $uri_host   = isset( $_SERVER['HTTP_HOST'] )
227                                ? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_DEFAULT )
228                                : (
229                                        isset( $_SERVER['SERVER_NAME'] )
230                                                ? filter_var( wp_unslash( $_SERVER['SERVER_NAME'] ), FILTER_DEFAULT )
231                                                : ''
232                                );
233                        $uri_port   = isset( $_SERVER['SERVER_PORT'] )
234                                ? filter_var( wp_unslash( $_SERVER['SERVER_PORT'] ), FILTER_SANITIZE_NUMBER_INT )
235                                : '';
236                        // we only need to include the port if it's non-standard
237                        if ( $uri_port && ( 'http' === $uri_scheme && '80' !== $uri_port || 'https' === $uri_scheme && '443' !== $uri_port ) ) {
238                                $uri_port = ':' . $uri_port;
239                        } else {
240                                $uri_port = '';
241                        }
242                        $this->url = array(
243                                $uri_scheme . '://' . $uri_host . $uri_port,
244                                $uri,
245                                $query_string,
246                        );
247                }
248                return $this->url;
249        }
250
251        /**
252         * Get the requested URI
253         *
254         * @param boolean $include_host If true, the scheme and domain will be included in the returned string (i.e. 'https://wordpress.com/index.php).
255         *                              If false, only the requested URI path will be returned (i.e. '/index.php').
256         * @return string
257         */
258        public function get_uri( $include_host = false ) {
259                list( $host, $file, $query ) = $this->get_url();
260
261                return ( $include_host ? $host : '' ) . $file . $query;
262        }
263
264        /**
265         * Return the filename part of the request
266         *
267         * @example for 'https://wordpress.com/some/page?id=5', return '/some/page'
268         * @return string
269         */
270        public function get_filename() {
271                return $this->get_url()[1];
272        }
273
274        /**
275         * Return the basename part of the request
276         *
277         * @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
278         * @return string
279         */
280        public function get_basename() {
281                // Get the filename part of the request
282                $filename = $this->get_filename();
283                // Normalize slashes
284                $filename = str_replace( '\\', '/', $filename );
285                // Remove trailing slashes
286                $filename = rtrim( $filename, '/' );
287                // Return the basename
288                $offset = strrpos( $filename, '/' );
289                return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
290        }
291
292        /**
293         * Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
294         *
295         * @return string
296         */
297        public function get_query_string() {
298                return $this->get_url()[2];
299        }
300
301        /**
302         * Returns the request body.
303         *
304         * @return string
305         */
306        public function get_body() {
307                $body = file_get_contents( 'php://input' );
308                return false === $body ? '' : $body;
309        }
310
311        /**
312         * Returns the cookies
313         *
314         * @return array<string, string>
315         */
316        public function get_cookies() {
317                return flatten_array( $_COOKIE );
318        }
319
320        /**
321         * Returns the GET variables
322         *
323         * @return array<string, mixed|array>
324         */
325        public function get_get_vars() {
326                return flatten_array( $_GET );
327        }
328
329        /**
330         * Returns the POST variables
331         *
332         * @return array<string, mixed|array>
333         */
334        public function get_post_vars() {
335                // Attempt to decode JSON requests.
336                if ( strpos( $this->get_header( 'content-type' ), 'application/json' ) !== false ) {
337                        $decoded_json = json_decode( $this->get_body(), true ) ?? array();
338                        return flatten_array( $decoded_json, 'json', true );
339                }
340
341                return flatten_array( $_POST );
342        }
343
344        /**
345         * Returns the files that were uploaded with this request (i.e. what's in the $_FILES superglobal)
346         *
347         * @return RequestFile[]
348         */
349        public function get_files() {
350                $files = array();
351                foreach ( $_FILES as $field_name => $arr ) {
352                        // flatten the values in case we were given inputs with brackets
353                        foreach ( flatten_array( $arr ) as list( $arr_key, $arr_value ) ) {
354                                if ( $arr_key === 'name' ) {
355                                        // if this file was a simple (non-nested) name and unique, then just add it.
356                                        $files[] = array(
357                                                'name'     => $field_name,
358                                                'filename' => $arr_value,
359                                        );
360                                } elseif ( 'name[' === substr( $arr_key, 0, 5 ) ) {
361                                        // otherwise this was a file with a nested name and/or multiple files with the same name
362                                        $files[] = array(
363                                                'name'     => $field_name . substr( $arr_key, 4 ),
364                                                'filename' => $arr_value,
365                                        );
366                                }
367                        }
368                }
369                return $files;
370        }
371}
Note: See TracBrowser for help on using the repository browser.