Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckUserUtilityService
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 2
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getClientIPfromXFF
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
156
1<?php
2
3namespace MediaWiki\CheckUser\Services;
4
5use MediaWiki\Request\ProxyLookup;
6use MediaWiki\Request\WebRequest;
7use Wikimedia\IPUtils;
8
9class CheckUserUtilityService {
10
11    private ProxyLookup $proxyLookup;
12
13    private bool $usePrivateIPs;
14
15    /**
16     * @param ProxyLookup $proxyLookup
17     * @param bool $usePrivateIPs
18     */
19    public function __construct( ProxyLookup $proxyLookup, bool $usePrivateIPs ) {
20        $this->proxyLookup = $proxyLookup;
21        $this->usePrivateIPs = $usePrivateIPs;
22    }
23
24    /**
25     * Locates the client IP within a given XFF string.
26     * Unlike the XFF checking to determine a user IP in WebRequest,
27     * this simply follows the chain and does not account for server trust.
28     *
29     * This returns an array containing:
30     *   - The best guess of the client IP
31     *   - Whether all the proxies are just squid/varnish
32     *   - The XFF value, converted to a empty string if false
33     *
34     * @param string|bool $xff XFF header value
35     * @return array (string|null, bool, string)
36     */
37    public function getClientIPfromXFF( $xff ) {
38        if ( $xff === false || !strlen( $xff ) ) {
39            // If the XFF is empty or not a string return with a
40            // XFF of the empty string and no results
41            return [ null, false, '' ];
42        }
43
44        # Get the list in the form of <PROXY N, ... PROXY 1, CLIENT>
45        $ipchain = array_map( 'trim', explode( ',', $xff ) );
46        $ipchain = array_reverse( $ipchain );
47
48        // best guess of the client IP
49        $client = null;
50
51        // all proxy servers where site Squid/Varnish servers?
52        $isSquidOnly = false;
53        # Step through XFF list and find the last address in the list which is a
54        # sensible proxy server. Set $ip to the IP address given by that proxy server,
55        # unless the address is not sensible (e.g. private). However, prefer private
56        # IP addresses over proxy servers controlled by this site (more sensible).
57        foreach ( $ipchain as $i => $curIP ) {
58            $curIP = IPUtils::canonicalize(
59                WebRequest::canonicalizeIPv6LoopbackAddress( $curIP )
60            );
61            if ( $curIP === null ) {
62                // not a valid IP address
63                break;
64            }
65            $curIsSquid = $this->proxyLookup->isConfiguredProxy( $curIP );
66            if ( $client === null ) {
67                $client = $curIP;
68                $isSquidOnly = $curIsSquid;
69            }
70            if (
71                isset( $ipchain[$i + 1] ) &&
72                IPUtils::isIPAddress( $ipchain[$i + 1] ) &&
73                (
74                    IPUtils::isPublic( $ipchain[$i + 1] ) ||
75                    $this->usePrivateIPs ||
76                    // T50919
77                    $curIsSquid
78                )
79            ) {
80                $client = IPUtils::canonicalize(
81                    WebRequest::canonicalizeIPv6LoopbackAddress( $ipchain[$i + 1] )
82                );
83                $isSquidOnly = ( $isSquidOnly && $curIsSquid );
84                continue;
85            }
86            break;
87        }
88
89        return [ $client, $isSquidOnly, $xff ];
90    }
91}
92
93/**
94 * Retain the old namespace for backwards compatibility.
95 * @deprecated since 1.41
96 */
97class_alias( CheckUserUtilityService::class, 'MediaWiki\CheckUser\CheckUserUtilityService' );