Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 59 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
| Atomic_Record_Jetpack_Token_Errors | |
0.00% |
0 / 58 |
|
0.00% |
0 / 4 |
506 | |
0.00% |
0 / 1 |
| signature_error_header | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
| is_jetpack_request | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
| check_ip | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
| check_ipv4 | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Atomic_Record_Jetpack_Token_Errors file. |
| 4 | * |
| 5 | * @package wpcomsh |
| 6 | */ |
| 7 | |
| 8 | /** |
| 9 | * Logs Jetpack token errors as response headers. |
| 10 | */ |
| 11 | class Atomic_Record_Jetpack_Token_Errors { |
| 12 | /** |
| 13 | * $error is a WP_Error (always) and contains a "signature_details" data property. |
| 14 | * This is not limited to a fixed set of error codes: any Jetpack connection error |
| 15 | * reported with signature_details — signing-time errors like malformed_token, |
| 16 | * could_not_sign, invalid_nonce, or signature_mismatch, as well as token lookup |
| 17 | * errors like no_valid_blog_token, no_valid_user_token, or token_malformed — is |
| 18 | * logged here. |
| 19 | * |
| 20 | * @param WP_Error $error WP_Error instance. |
| 21 | */ |
| 22 | public static function signature_error_header( $error ) { |
| 23 | if ( headers_sent() ) { |
| 24 | return; |
| 25 | } |
| 26 | |
| 27 | if ( ! isset( $_SERVER['ATOMIC_SITE_ID'] ) && ! defined( 'ATOMIC_SITE_ID' ) ) { |
| 28 | return; |
| 29 | } |
| 30 | |
| 31 | if ( ! self::is_jetpack_request() ) { |
| 32 | return; |
| 33 | } |
| 34 | |
| 35 | $error_data = $error->get_error_data(); |
| 36 | if ( ! isset( $error_data['signature_details'] ) ) { |
| 37 | return; |
| 38 | } |
| 39 | header( |
| 40 | sprintf( |
| 41 | 'X-Jetpack-Signature-Error: %s', |
| 42 | $error->get_error_code() |
| 43 | ) |
| 44 | ); |
| 45 | header( |
| 46 | sprintf( |
| 47 | 'X-Jetpack-Signature-Error-Message: %s', |
| 48 | $error->get_error_message() |
| 49 | ) |
| 50 | ); |
| 51 | header( |
| 52 | sprintf( |
| 53 | 'X-Jetpack-Signature-Error-Details: %s', |
| 54 | base64_encode( wp_json_encode( $error_data['signature_details'], JSON_UNESCAPED_SLASHES ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 55 | ) |
| 56 | ); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Checks the IP to see if it's a Jetpack request. |
| 61 | * |
| 62 | * Stolen from https://github.com/Automattic/vip-go-mu-plugins/pull/1301. |
| 63 | * |
| 64 | * @return bool |
| 65 | */ |
| 66 | public static function is_jetpack_request() { |
| 67 | // Filter by env. |
| 68 | if ( defined( 'WP_CLI' ) && WP_CLI ) { |
| 69 | return false; |
| 70 | } |
| 71 | |
| 72 | // Simple UA check to filter out most. |
| 73 | if ( false === stripos( $_SERVER['HTTP_USER_AGENT'], 'wpcomsh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 74 | return false; |
| 75 | } |
| 76 | |
| 77 | // If it has a valid-looking UA, check the remote IP. |
| 78 | // From https://jetpack.com/support/hosting-faq/#jetpack-whitelist |
| 79 | $jetpack_ips = array( |
| 80 | '122.248.245.244', |
| 81 | '54.217.201.243', |
| 82 | '54.232.116.4', |
| 83 | '192.0.80.0/20', |
| 84 | '192.0.96.0/20', |
| 85 | '192.0.112.0/20', |
| 86 | '195.234.108.0/22', |
| 87 | ); |
| 88 | |
| 89 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 90 | return self::check_ip( $_SERVER['REMOTE_ADDR'], $jetpack_ips ) || ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && self::check_ip( $_SERVER['HTTP_X_FORWARDED_FOR'], $jetpack_ips ) ); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. |
| 95 | * |
| 96 | * @param string $request_ip IP to check. |
| 97 | * @param string|array $ips List of IPs or subnets (can be a string if only a single one). |
| 98 | * |
| 99 | * @return bool Whether the IP is valid. |
| 100 | */ |
| 101 | public static function check_ip( $request_ip, $ips ) { |
| 102 | if ( ! is_array( $ips ) ) { |
| 103 | $ips = array( $ips ); |
| 104 | } |
| 105 | |
| 106 | foreach ( $ips as $ip ) { |
| 107 | if ( self::check_ipv4( $request_ip, $ip ) ) { |
| 108 | return true; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | return false; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Compares two IPv4 addresses. |
| 117 | * In case a subnet is given, it checks if it contains the request IP. |
| 118 | * |
| 119 | * @param string $request_ip IPv4 address to check. |
| 120 | * @param string $ip IPv4 address or subnet in CIDR notation. |
| 121 | * |
| 122 | * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet. |
| 123 | */ |
| 124 | public static function check_ipv4( $request_ip, $ip ) { |
| 125 | if ( ! filter_var( $request_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { |
| 126 | return false; |
| 127 | } |
| 128 | if ( false !== strpos( $ip, '/' ) ) { |
| 129 | list( $address, $netmask ) = explode( '/', $ip, 2 ); |
| 130 | if ( $netmask === '0' ) { |
| 131 | return filter_var( $address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ); |
| 132 | } |
| 133 | if ( $netmask < 0 || $netmask > 32 ) { |
| 134 | return false; |
| 135 | } |
| 136 | } else { |
| 137 | $address = $ip; |
| 138 | $netmask = 32; |
| 139 | } |
| 140 | |
| 141 | return 0 === substr_compare( sprintf( '%032b', ip2long( $request_ip ) ), sprintf( '%032b', ip2long( $address ) ), 0, $netmask ); |
| 142 | } |
| 143 | } |
| 144 | add_action( 'jetpack_verify_signature_error', array( 'Atomic_Record_Jetpack_Token_Errors', 'signature_error_header' ) ); |