Completed
Push — scrutinizer ( df6929...9bdd35 )
by Jonathan
02:31
created

Whip::isMethodUsable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4286
cc 3
eloc 6
nc 3
nop 2
crap 3
1
<?php
2
3
/*
4
The MIT License (MIT)
5
6
Copyright (c) 2015 Vectorface, Inc.
7
8
Permission is hereby granted, free of charge, to any person obtaining a copy
9
of this software and associated documentation files (the "Software"), to deal
10
in the Software without restriction, including without limitation the rights
11
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
copies of the Software, and to permit persons to whom the Software is
13
furnished to do so, subject to the following conditions:
14
15
The above copyright notice and this permission notice shall be included in
16
all copies or substantial portions of the Software.
17
18
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
THE SOFTWARE.
25
*/
26
27
namespace Vectorface\Whip;
28
29
use \Exception;
30
use Vectorface\Whip\IpRange\IpWhitelist;
31
use Vectorface\Whip\Request\RequestAdapter;
32
use Vectorface\Whip\Request\Psr7RequestAdapter;
33
use Vectorface\Whip\Request\SuperglobalRequestAdapter;
34
use Psr\Http\Message\ServerRequestInterface;
35
36
/**
37
 * A class for accurately looking up a client's IP address.
38
 * This class checks a call time configurable list of headers in the $_SERVER
39
 * superglobal to determine the client's IP address.
40
 * @copyright Vectorface, Inc 2015
41
 * @author Daniel Bruce <[email protected]>
42
 * @author Cory Darby <[email protected]>
43
 */
44
class Whip
45
{
46
47
    /** Indicates all header methods will be used. */
48
    const ALL_METHODS        = 255;
49
    /** Indicates the REMOTE_ADDR method will be used. */
50
    const REMOTE_ADDR        = 1;
51
    /** Indicates a set of possible proxy headers will be used. */
52
    const PROXY_HEADERS      = 2;
53
    /** Indicates any CloudFlare specific headers will be used. */
54
    const CLOUDFLARE_HEADERS = 4;
55
    /** Indicates any Incapsula specific headers will be used. */
56
    const INCAPSULA_HEADERS  = 8;
57
    /** Indicates custom listed headers will be used. */
58
    const CUSTOM_HEADERS     = 128;
59
60
    /** The array of mapped header strings. */
61
    private static $headers = array(
62
        self::CUSTOM_HEADERS     => array(),
63
        self::INCAPSULA_HEADERS  => array(
64
            'incap-client-ip'
65
        ),
66
        self::CLOUDFLARE_HEADERS => array(
67
            'cf-connecting-ip'
68
        ),
69
        self::PROXY_HEADERS      => array(
70
            'client-ip',
71
            'x-forwarded-for',
72
            'x-forwarded',
73
            'x-cluster-client-ip',
74
            'forwarded-for',
75
            'forwarded',
76
            'x-real-ip',
77
        ),
78
    );
79
80
    /** the bitmask of enabled methods */
81
    private $enabled;
82
83
    /** the array of IP whitelist ranges to check against */
84
    private $whitelist;
85
86
    /**
87
     * An object holding the source of addresses we will check
88
     *
89
     * @var RequestAdapter
90
     */
91
    private $source;
92
93
    /**
94
     * Constructor for the class.
95
     * @param int $enabled The bitmask of enabled headers.
96
     * @param array $whitelists The array of IP ranges to be whitelisted.
97
     */
98 22
    public function __construct($enabled = self::ALL_METHODS, array $whitelists = array(), $source = null)
99
    {
100 22
        $this->enabled   = (int) $enabled;
101 22
        if (isset($source)) {
102 16
            $this->setSource($source);
103 15
        }
104 21
        $this->whitelist = array();
105 21
        foreach ($whitelists as $header => $ipRanges) {
106 12
            $this->whitelist[$header] = new IpWhitelist($ipRanges);
107 21
        }
108 21
    }
109
110
    /**
111
     * Adds a custom header to the list.
112
     * @param string $header The custom header to add.
113
     * @return Whip Returns $this.
114
     */
115 1
    public function addCustomHeader($header)
116
    {
117 1
        if (strpos($header, 'HTTP_') === 0) {
118 1
            $header = str_replace('_', '-', substr($header, 5));
119 1
        }
120 1
        self::$headers[self::CUSTOM_HEADERS][] = strtolower($header);
121 1
        return $this;
122
    }
123
124
    /**
125
     * Get a source adapter for a given source of IP data.
126
     *
127
     * @param mixed $source
128
     * @return RequestAdapter
129
     */
130 22
    private function getRequestAdapter($source)
131
    {
132 22
        if ($source instanceof RequestAdapter) {
133 19
            return $source;
134 22
        } elseif ($source instanceof ServerRequestInterface) {
135 1
            return new Psr7RequestAdapter($source);
136 21
        } elseif (is_array($source)) {
137 20
            return new SuperglobalRequestAdapter($source);
138
        }
139
140 1
        throw new \InvalidArgumentException("Unknown IP source.");
141
    }
142
143
    /**
144
     * Given available sources, get the best source of IP data.
145
     *
146
     * @param mixed $source A source data argument.
147
     * @return mixed The best available source, after fallbacks.
148
     */
149 21
    private function coalesceSources($source = null)
0 ignored issues
show
Coding Style introduced by
coalesceSources uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
150
    {
151 21
        if (isset($source)) {
152 1
            return $source;
153 21
        } elseif (isset($this->source)) {
154 19
            return $this->source;
155
        }
156
157 2
        return $_SERVER;
158
    }
159
160
    /**
161
     * Sets the source data used to lookup the addresses.
162
     *
163
     * @param $source The source array.
164
     * @return Whip Returns $this.
165
     */
166 20
    public function setSource($source)
167
    {
168 20
        $this->source = static::getRequestAdapter($source);
0 ignored issues
show
Bug introduced by
Since getRequestAdapter() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of getRequestAdapter() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
169
170 19
        return $this;
171
    }
172
173
    /**
174
     * Returns the IP address of the client using the given methods.
175
     * @param array $source (optional) The source array. By default, the class
176
     *        will use the value passed to Whip::setSource or fallback to
177
     *        $_SERVER.
178
     * @return string Returns the IP address as a string or false if no
179
     *         IP address could be found.
180
     */
181 21
    public function getIpAddress($source = null)
182
    {
183 21
        $source = $this->getRequestAdapter($this->coalesceSources($source));
184 21
        $remoteAddr = $source->getRemoteAddr();
185 21
        $clientHeaders = $source->getHeaders();
186
187 21
        foreach (self::$headers as $key => $headers) {
188 21
            if (!$this->isMethodUsable($key, $remoteAddr)) {
189 19
                continue;
190
            }
191 11
            return $this->extractAddressFromHeaders($clientHeaders, $headers);
192 10
        }
193
194 10
        return ($this->enabled & self::REMOTE_ADDR) ? $remoteAddr : false;
195
    }
196
197
    /**
198
     * Returns the valid IP address or false if no valid IP address was found.
199
     * @param array $source (optional) The source array. By default, the class
200
     *        will use the value passed to Whip::setSource or fallback to
201
     *        $_SERVER.
202
     * @return string|false Returns the IP address (as a string) of the client or false
203
     *         if no valid IP address was found.
204
     */
205 4
    public function getValidIpAddress($source = null)
206
    {
207 4
        $ipAddress = $this->getIpAddress($source);
208 4
        if (false === $ipAddress || false === @inet_pton($ipAddress)) {
209 1
            return false;
210
        }
211 3
        return $ipAddress;
212
    }
213
214
    /**
215
     * Finds the first element in $headers that is present in $_SERVER and
216
     * returns the IP address mapped to that value.
217
     * If the IP address is a list of comma separated values, the last value
218
     * in the list will be returned.
219
     * If no IP address is found, we return false.
220
     * @param array $source  The source array to pull the data from.
221
     * @param array $headers The list of headers to check.
222
     * @return string|false Returns the IP address as a string or false if no IP
223
     *         IP address was found.
224
     */
225 11
    private function extractAddressFromHeaders($source, $headers)
226
    {
227 11
        foreach ($headers as $header) {
228 10
            if (empty($source[$header])) {
229 9
                continue;
230
            }
231 9
            $list = explode(',', $source[$header]);
232 9
            return trim(end($list));
233 2
        }
234 2
        return false;
235
    }
236
237
    /**
238
     * Returns whether or not the given method is enabled and usable.
239
     *
240
     * This method checks if the method is enabled and whether the method's data
241
     * is usable given it's IP whitelist.
242
     *
243
     * @param string $key The source key.
244
     * @param string $ipAddress The IP address.
245
     * @return boolean Returns true if the IP address is whitelisted and false
246
     *         otherwise. Returns true if the source does not have a whitelist
247
     *         specified.
248
     */
249 21
    private function isMethodUsable($key, $ipAddress)
250
    {
251 21
        if (!($key & $this->enabled)) {
252 19
            return false;
253
        }
254 15
        if (!isset($this->whitelist[$key])) {
255 3
            return true;
256
        }
257 12
        return $this->whitelist[$key]->isIpWhitelisted($ipAddress);
258
    }
259
}
260