Completed
Push — renovate/debug-4.x ( e10916...91e0c8 )
by
unknown
86:28 queued 77:02
created

Jetpack_Signature::get_current_request_port()   B

Complexity

Conditions 9
Paths 32

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 32
nop 0
dl 0
loc 33
rs 8.0555
c 0
b 0
f 0
1
<?php
2
/**
3
 * The Jetpack Connection signature class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
9
10
/**
11
 * The Jetpack Connection signature class that is used to sign requests.
12
 */
13
class Jetpack_Signature {
14
	/**
15
	 * Token part of the access token.
16
	 *
17
	 * @access public
18
	 * @var string
19
	 */
20
	public $token;
21
22
	/**
23
	 * Access token secret.
24
	 *
25
	 * @access public
26
	 * @var string
27
	 */
28
	public $secret;
29
30
	/**
31
	 * The current request URL.
32
	 *
33
	 * @access public
34
	 * @var string
35
	 */
36
	public $current_request_url;
37
38
	/**
39
	 * Constructor.
40
	 *
41
	 * @param array $access_token Access token.
42
	 * @param int   $time_diff    Timezone difference (in seconds).
43
	 */
44
	public function __construct( $access_token, $time_diff = 0 ) {
45
		$secret = explode( '.', $access_token );
46
		if ( 2 !== count( $secret ) ) {
47
			return;
48
		}
49
50
		$this->token     = $secret[0];
51
		$this->secret    = $secret[1];
52
		$this->time_diff = $time_diff;
0 ignored issues
show
Bug introduced by
The property time_diff does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53
	}
54
55
	/**
56
	 * Sign the current request.
57
	 *
58
	 * @todo Implement a proper nonce verification.
59
	 *
60
	 * @param array $override Optional arguments to override the ones from the current request.
61
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
62
	 */
63
	public function sign_current_request( $override = array() ) {
64
		if ( isset( $override['scheme'] ) ) {
65
			$scheme = $override['scheme'];
66
			if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
67
				return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_scheme'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
68
			}
69
		} else {
70
			if ( is_ssl() ) {
71
				$scheme = 'https';
72
			} else {
73
				$scheme = 'http';
74
			}
75
		}
76
77
		$port = $this->get_current_request_port();
78
79
		$this->current_request_url = "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . stripslashes( $_SERVER['REQUEST_URI'] );
80
81
		if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) {
82
			$body = $override['body'];
83
		} elseif ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
84
			$body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null;
85
86
			// Convert the $_POST to the body, if the body was empty. This is how arrays are hashed
87
			// and encoded on the Jetpack side.
88
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
89
				// phpcs:ignore WordPress.Security.NonceVerification.Missing
90
				if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) {
91
					$body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
92
				}
93
			}
94
		} elseif ( 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
95
			// This is a little strange-looking, but there doesn't seem to be another way to get the PUT body.
96
			$raw_put_data = file_get_contents( 'php://input' );
97
			parse_str( $raw_put_data, $body );
98
99
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
100
				$put_data = json_decode( $raw_put_data, true );
101
				if ( is_array( $put_data ) && count( $put_data ) > 0 ) {
102
					$body = $put_data;
103
				}
104
			}
105
		} else {
106
			$body = null;
107
		}
108
109
		if ( empty( $body ) ) {
110
			$body = null;
111
		}
112
113
		$a = array();
114
		foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) {
115
			if ( isset( $override[ $parameter ] ) ) {
116
				$a[ $parameter ] = $override[ $parameter ];
117
			} else {
118
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
119
				$a[ $parameter ] = isset( $_GET[ $parameter ] ) ? stripslashes( $_GET[ $parameter ] ) : '';
120
			}
121
		}
122
123
		$method = isset( $override['method'] ) ? $override['method'] : $_SERVER['REQUEST_METHOD'];
124
		return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true );
125
	}
126
127
	/**
128
	 * Sign a specified request.
129
	 *
130
	 * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array?
131
	 * @todo Use wp_json_encode() instead of json_encode()?
132
	 *
133
	 * @param string $token            Request token.
134
	 * @param int    $timestamp        Timestamp of the request.
135
	 * @param string $nonce            Request nonce.
136
	 * @param string $body_hash        Request body hash.
137
	 * @param string $method           Request method.
138
	 * @param string $url              Request URL.
139
	 * @param mixed  $body             Request body.
140
	 * @param bool   $verify_body_hash Whether to verify the body hash against the body.
141
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
142
	 */
143
	public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) {
144
		if ( ! $this->secret ) {
145
			return new WP_Error( 'invalid_secret', 'Invalid secret' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_secret'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
146
		}
147
148
		if ( ! $this->token ) {
149
			return new WP_Error( 'invalid_token', 'Invalid token' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
150
		}
151
152
		list( $token ) = explode( '.', $token );
153
154
		$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );
155
156
		if ( 0 !== strpos( $token, "$this->token:" ) ) {
157
			return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'token_mismatch'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
158
		}
159
160
		// If we got an array at this point, let's encode it, so we can see what it looks like as a string.
161
		if ( is_array( $body ) ) {
162
			if ( count( $body ) > 0 ) {
163
				// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
164
				$body = json_encode( $body );
165
166
			} else {
167
				$body = '';
168
			}
169
		}
170
171
		$required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' );
172
		if ( ! is_null( $body ) ) {
173
			$required_parameters[] = 'body_hash';
174
			if ( ! is_string( $body ) ) {
175
				return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
176
			}
177
		}
178
179
		foreach ( $required_parameters as $required ) {
180
			if ( ! is_scalar( $$required ) ) {
181
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
182
			}
183
184 View Code Duplication
			if ( ! strlen( $$required ) ) {
185
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
186
			}
187
		}
188
189
		if ( empty( $body ) ) {
190
			if ( $body_hash ) {
191
				return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body_hash'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
192
			}
193
		} else {
194
			$connection = new Connection_Manager();
195
			if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) {
196
				return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body_hash'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
197
			}
198
		}
199
200
		$parsed = wp_parse_url( $url );
201
		if ( ! isset( $parsed['host'] ) ) {
202
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
203
		}
204
205
		if ( ! empty( $parsed['port'] ) ) {
206
			$port = $parsed['port'];
207
		} else {
208
			if ( 'http' === $parsed['scheme'] ) {
209
				$port = 80;
210
			} elseif ( 'https' === $parsed['scheme'] ) {
211
				$port = 443;
212
			} else {
213
				return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown_scheme_port'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
214
			}
215
		}
216
217 View Code Duplication
		if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug.
218
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
219
		}
220
221
		$local_time = $timestamp - $this->time_diff;
222 View Code Duplication
		if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
223
			return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
224
		}
225
226 View Code Duplication
		if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) {
227
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228
		}
229
230
		$normalized_request_pieces = array(
231
			$token,
232
			$timestamp,
233
			$nonce,
234
			$body_hash,
235
			strtoupper( $method ),
236
			strtolower( $parsed['host'] ),
237
			$port,
238
			$parsed['path'],
239
			// Normalized Query String.
240
		);
241
242
		$normalized_request_pieces      = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) );
243
		$flat_normalized_request_pieces = array();
244
		foreach ( $normalized_request_pieces as $piece ) {
245
			if ( is_array( $piece ) ) {
246
				foreach ( $piece as $subpiece ) {
247
					$flat_normalized_request_pieces[] = $subpiece;
248
				}
249
			} else {
250
				$flat_normalized_request_pieces[] = $piece;
251
			}
252
		}
253
		$normalized_request_pieces = $flat_normalized_request_pieces;
254
255
		$normalized_request_string = join( "\n", $normalized_request_pieces ) . "\n";
256
257
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
258
		return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) );
259
	}
260
261
	/**
262
	 * Retrieve and normalize the parameters from a query string.
263
	 *
264
	 * @param string $query_string Query string.
265
	 * @return array Normalized query string parameters.
266
	 */
267
	public function normalized_query_parameters( $query_string ) {
268
		parse_str( $query_string, $array );
269
270
		unset( $array['signature'] );
271
272
		$names  = array_keys( $array );
273
		$values = array_values( $array );
274
275
		$names  = array_map( array( $this, 'encode_3986' ), $names );
276
		$values = array_map( array( $this, 'encode_3986' ), $values );
277
278
		$pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values );
279
280
		sort( $pairs );
281
282
		return $pairs;
283
	}
284
285
	/**
286
	 * Encodes a string or array of strings according to RFC 3986.
287
	 *
288
	 * @param string|array $string_or_array String or array to encode.
289
	 * @return string|array URL-encoded string or array.
290
	 */
291
	public function encode_3986( $string_or_array ) {
292
		if ( is_array( $string_or_array ) ) {
293
			return array_map( array( $this, 'encode_3986' ), $string_or_array );
294
		}
295
296
		return rawurlencode( $string_or_array );
297
	}
298
299
	/**
300
	 * Concatenates a parameter name and a parameter value with an equals sign between them.
301
	 *
302
	 * @param string       $name  Parameter name.
303
	 * @param string|array $value Parameter value.
304
	 * @return string|array A string pair (e.g. `name=value`) or an array of string pairs.
305
	 */
306
	public function join_with_equal_sign( $name, $value ) {
307
		if ( is_array( $value ) ) {
308
			return $this->join_array_with_equal_sign( $name, $value );
309
		}
310
		return "{$name}={$value}";
311
	}
312
313
	/**
314
	 * Helper function for join_with_equal_sign for handling arrayed values.
315
	 * Explicitly supports nested arrays.
316
	 *
317
	 * @param string $name  Parameter name.
318
	 * @param array  $value Parameter value.
319
	 * @return array An array of string pairs (e.g. `[ name[example]=value ]`).
320
	 */
321
	private function join_array_with_equal_sign( $name, $value ) {
322
		$result = array();
323
		foreach ( $value as $value_key => $value_value ) {
324
			$joined_value = $this->join_with_equal_sign( $name . '[' . $value_key . ']', $value_value );
325
			if ( is_array( $joined_value ) ) {
326
				foreach ( array_values( $joined_value ) as $individual_joined_value ) {
327
					$result[] = $individual_joined_value;
328
				}
329
			} elseif ( is_string( $joined_value ) ) {
330
				$result[] = $joined_value;
331
			}
332
		}
333
334
		sort( $result );
335
		return $result;
336
	}
337
338
	/**
339
	 * Gets the port that should be considered to sign the current request.
340
	 *
341
	 * It will analyze the current request, as well as some Jetpack constants, to return the string
342
	 * to be concatenated in the URL representing the port of the current request.
343
	 *
344
	 * @since 9.2.0
345
	 *
346
	 * @return string The port to be used in the signature
347
	 */
348
	public function get_current_request_port() {
349
		$host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $this->sanitize_host_post( $_SERVER['HTTP_X_FORWARDED_PORT'] ) : '';
350
		if ( '' === $host_port && isset( $_SERVER['SERVER_PORT'] ) ) {
351
			$host_port = $this->sanitize_host_post( $_SERVER['SERVER_PORT'] );
352
		}
353
354
		/**
355
		 * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test.
356
		 * Please update the test if any changes are made in this logic.
357
		 */
358
		if ( is_ssl() ) {
359
			// 443: Standard Port
360
			// 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites
361
			// with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with
362
			// the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a
363
			// site at https://example.com:80/ (which would be a strange configuration).
364
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
365
			// if the site is behind a proxy running on port 443 without
366
			// X-Forwarded-Port and the back end's port is *not* 80. It's better,
367
			// though, to configure the proxy to send X-Forwarded-Port.
368
			$https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTPS_PORT ) : '443';
369
			$port       = in_array( $host_port, array( '443', '80', $https_port ), true ) ? '' : $host_port;
370
		} else {
371
			// 80: Standard Port
372
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
373
			// if the site is behind a proxy running on port 80 without
374
			// X-Forwarded-Port. It's better, though, to configure the proxy to
375
			// send X-Forwarded-Port.
376
			$http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? $this->sanitize_host_post( JETPACK_SIGNATURE__HTTP_PORT ) : '80';
377
			$port      = in_array( $host_port, array( '80', $http_port ), true ) ? '' : $host_port;
378
		}
379
		return (string) $port;
380
	}
381
382
	/**
383
	 * Sanitizes a variable checking if it's a valid port number, which can be an integer or a numeric string
384
	 *
385
	 * @since 9.2.0
386
	 *
387
	 * @param mixed $port_number Variable representing a port number.
388
	 * @return string Always a string with a valid port number, or an empty string if input is invalid
389
	 */
390
	public function sanitize_host_post( $port_number ) {
391
392
		if ( ! is_int( $port_number ) && ! is_string( $port_number ) ) {
393
			return '';
394
		}
395
		if ( is_string( $port_number ) && ! ctype_digit( $port_number ) ) {
396
			return '';
397
		}
398
399
		if ( 0 >= (int) $port_number || 65535 < $port_number ) {
400
			return '';
401
		}
402
		return (string) $port_number;
403
	}
404
}
405