Completed
Push — next ( bac8c4...aa6b76 )
by Jonathan
02:49
created

Whip   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 197
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 92.86%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 197
wmc 25
lcom 1
cbo 2
ccs 52
cts 56
cp 0.9286
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
A addCustomHeader() 0 8 2
B getSourceAdapter() 0 14 5
A setSource() 0 6 1
B getIpAddress() 0 17 6
A getValidIpAddress() 0 8 3
A extractAddressFromHeaders() 0 11 3
A isIpWhitelisted() 0 7 2
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 18
    public function __construct($enabled = self::ALL_METHODS, array $whitelists = array(), $source = null)
99
    {
100 18
        $this->enabled   = (int) $enabled;
101 18
        if (isset($source)) {
102
            $this->setSource($source);
103
        }
104 18
        $this->whitelist = array();
105 18
        foreach ($whitelists as $header => $ipRanges) {
106 10
            $this->whitelist[$header] = new IpWhitelist($ipRanges);
107 18
        }
108 18
    }
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
     * @param bool $fallback If true, fallback to $_SERVER if source is unusable.
129
     */
130 18
    public static function getSourceAdapter($source, $fallback = false)
0 ignored issues
show
Coding Style introduced by
getSourceAdapter 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...
131
    {
132 18
        if ($source instanceof RequestAdapter) {
133 1
            return $source;
134 18
        } elseif ($source instanceof ServerRequestInterface) {
0 ignored issues
show
Bug introduced by
The class Psr\Http\Message\ServerRequestInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
135
            return new Psr7RequestAdapter($source);
136 18
        } elseif (is_array($source)) {
137 2
            return new SuperglobalRequestAdapter($source);
138 18
        } elseif ($fallback) {
139 18
            return new SuperglobalRequestAdapter($_SERVER);
140
        }
141
142
        throw new \InvalidArgumentException("Unknown IP source.");
143
    }
144
145
    /**
146
     * Sets the source data used to lookup the addresses.
147
     *
148
     * @param $source The source array.
149
     * @return Whip Returns $this.
150
     */
151 1
    public function setSource($source)
152
    {
153 1
        $this->source = static::getSourceAdapter($source);
0 ignored issues
show
Documentation Bug introduced by
It seems like static::getSourceAdapter($source) can also be of type object<Vectorface\Whip\R...est\Psr7RequestAdapter>. However, the property $source is declared as type object<Vectorface\Whip\Request\RequestAdapter>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
154
155 1
        return $this;
156
    }
157
158
    /**
159
     * Returns the IP address of the client using the given methods.
160
     * @param array $source (optional) The source array. By default, the class
161
     *        will use the value passed to Whip::setSource or fallback to
162
     *        $_SERVER.
163
     * @return string Returns the IP address as a string or false if no
164
     *         IP address could be found.
165
     */
166 18
    public function getIpAddress($source = null)
167
    {
168 18
        $source = $source ? static::getSourceAdapter($source) : static::getSourceAdapter($this->source, true);
169 18
        $remoteAddr = $source->getRemoteAddr();
170 18
        $clientHeaders = $source->getHeaders();
171
172 18
        foreach (self::$headers as $key => $headers) {
173 18
            if (!($key & $this->enabled) || !$this->isIpWhitelisted($key, $remoteAddr)) {
174
                // skip this header if not enabled or if the local address
175
                // is not whitelisted
176 16
                continue;
177
            }
178 9
            return $this->extractAddressFromHeaders($clientHeaders, $headers);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->extractAddressFro...ientHeaders, $headers); of type string|false adds false to the return on line 178 which is incompatible with the return type documented by Vectorface\Whip\Whip::getIpAddress of type string. It seems like you forgot to handle an error condition.
Loading history...
179 9
        }
180
181 9
        return ($this->enabled & self::REMOTE_ADDR) ? $remoteAddr : false;
182
    }
183
184
    /**
185
     * Returns the valid IP address or false if no valid IP address was found.
186
     * @param array $source (optional) The source array. By default, the class
187
     *        will use the value passed to Whip::setSource or fallback to
188
     *        $_SERVER.
189
     * @return string|false Returns the IP address (as a string) of the client or false
190
     *         if no valid IP address was found.
191
     */
192 3
    public function getValidIpAddress($source = null)
193
    {
194 3
        $ipAddress = $this->getIpAddress($source);
195 3
        if (false === $ipAddress || false === @inet_pton($ipAddress)) {
196 1
            return false;
197
        }
198 2
        return $ipAddress;
199
    }
200
201
    /**
202
     * Finds the first element in $headers that is present in $_SERVER and
203
     * returns the IP address mapped to that value.
204
     * If the IP address is a list of comma separated values, the last value
205
     * in the list will be returned.
206
     * If no IP address is found, we return false.
207
     * @param array $source  The source array to pull the data from.
208
     * @param array $headers The list of headers to check.
209
     * @return string|false Returns the IP address as a string or false if no IP
210
     *         IP address was found.
211
     */
212 9
    private function extractAddressFromHeaders($source, $headers)
213
    {
214 9
        foreach ($headers as $header) {
215 8
            if (empty($source[$header])) {
216 7
                continue;
217
            }
218 7
            $list = explode(',', $source[$header]);
219 7
            return trim(end($list));
220 2
        }
221 2
        return false;
222
    }
223
224
    /**
225
     * Returns whether or not the given IP address is whitelisted for the given
226
     * source key.
227
     * @param string $key The source key.
228
     * @param string $ipAddress The IP address.
229
     * @return boolean Returns true if the IP address is whitelisted and false
230
     *         otherwise. Returns true if the source does not have a whitelist
231
     *         specified.
232
     */
233 13
    private function isIpWhitelisted($key, $ipAddress)
234
    {
235 13
        if (!isset($this->whitelist[$key])) {
236 3
            return true;
237
        }
238 10
        return $this->whitelist[$key]->isIpWhitelisted($ipAddress);
239
    }
240
}
241