callable,'forward'=>callable] for tests. * @return array Verdict with keys event_class, verification_method, verification_result. */ public static function classify( $ip, array $agent, $resolver = null ) { $cidrs = isset( $agent['cidrs'] ) ? (array) $agent['cidrs'] : array(); $suffixes = isset( $agent['reverse_dns_suffixes'] ) ? (array) $agent['reverse_dns_suffixes'] : array(); if ( ! empty( $cidrs ) && AIMention_IP_Resolver::ip_in_any_range( $ip, $cidrs ) ) { return self::result( 'verified_declared', 'ip_range', 'pass' ); } if ( ! empty( $suffixes ) && self::reverse_dns_confirms( $ip, $suffixes, $resolver ) ) { return self::result( 'verified_declared', 'reverse_dns', 'pass' ); } // Could we verify at all? If the agent carries ranges or suffixes and none // matched, this is a claimed-but-unverified crawler -> excluded. if ( ! empty( $cidrs ) || ! empty( $suffixes ) ) { $method = ! empty( $cidrs ) ? 'ip_range' : 'reverse_dns'; return self::result( 'failed_verification', $method, 'fail' ); } // Nothing to verify against (an unconfirmed agent): declare, stay unconfirmed. return self::result( 'verified_declared', 'none', 'unconfirmed' ); } /** * Forward-confirmed reverse DNS: PTR ends in an approved suffix AND resolves * forward back to the same IP. * * @param string $ip Crawler IP. * @param array $suffixes Approved reverse-DNS suffixes. * @param array|null $resolver Optional injected ['ptr'=>callable,'forward'=>callable] for tests. * @return bool True if the IP is forward-confirmed. */ private static function reverse_dns_confirms( $ip, array $suffixes, $resolver ) { $ptr_fn = ( is_array( $resolver ) && isset( $resolver['ptr'] ) ) ? $resolver['ptr'] : 'gethostbyaddr'; $forward_fn = ( is_array( $resolver ) && isset( $resolver['forward'] ) ) ? $resolver['forward'] : array( __CLASS__, 'forward_lookup' ); $ptr = call_user_func( $ptr_fn, $ip ); if ( ! is_string( $ptr ) || '' === $ptr || $ptr === $ip ) { return false; } if ( ! self::host_matches_suffix( $ptr, $suffixes ) ) { return false; } $forward = (array) call_user_func( $forward_fn, $ptr ); return in_array( $ip, $forward, true ); } /** * Whether a host equals, or is a proper sub-domain of, an approved suffix. * * @param string $host Hostname (e.g. a PTR result). * @param array $suffixes Approved suffixes. * @return bool True on an exact or boundary-safe sub-domain match. */ public static function host_matches_suffix( $host, array $suffixes ) { $host = strtolower( rtrim( (string) $host, '.' ) ); foreach ( $suffixes as $suffix ) { $suffix = strtolower( trim( ltrim( (string) $suffix, '.' ) ) ); if ( '' === $suffix ) { continue; } // Equal, or a proper sub-domain -- never "evilgooglebot.com". if ( $host === $suffix || self::str_ends_with( $host, '.' . $suffix ) ) { return true; } } return false; } /** * Resolve a hostname to its A + AAAA addresses. * * @param string $host Hostname to resolve. * @return array List of resolved IP addresses. */ public static function forward_lookup( $host ) { $ips = array(); if ( function_exists( 'dns_get_record' ) ) { foreach ( array( DNS_A, DNS_AAAA ) as $type ) { $records = @dns_get_record( $host, $type ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- lookup failures are expected and handled via the is_array() check. if ( is_array( $records ) ) { foreach ( $records as $r ) { if ( ! empty( $r['ip'] ) ) { $ips[] = $r['ip']; } elseif ( ! empty( $r['ipv6'] ) ) { $ips[] = $r['ipv6']; } } } } } if ( empty( $ips ) ) { $v4 = @gethostbyname( $host ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- a failed lookup returns the host unchanged and is handled below. if ( $v4 && $v4 !== $host ) { $ips[] = $v4; } } return $ips; } /** * PHP 7.2-safe str_ends_with. * * @param string $haystack String to test. * @param string $needle Suffix to look for. * @return bool True if $haystack ends with $needle. */ private static function str_ends_with( $haystack, $needle ) { $len = strlen( $needle ); return 0 === $len || substr( $haystack, -$len ) === $needle; } /** * Assemble a verdict array. * * @param string $event_class Event class. * @param string $method Verification method. * @param string $result Verification result. * @return array The verdict. */ private static function result( $event_class, $method, $result ) { return array( 'event_class' => $event_class, 'verification_method' => $method, 'verification_result' => $result, ); } }