Completed
Push — dev ( 3aadaf...6a5b86 )
by Jonathan
13s
created

Whip::normalizeHeaderName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 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
    /** The whitelist key for IPv4 addresses */
47
    const IPV4 = IpWhitelist::IPV4;
48
49
    /** The whitelist key for IPv6 addresses */
50
    const IPV6 = IpWhitelist::IPV6;
51
52
    /** Indicates all header methods will be used. */
53
    const ALL_METHODS        = 255;
54
    /** Indicates the REMOTE_ADDR method will be used. */
55
    const REMOTE_ADDR        = 1;
56
    /** Indicates a set of possible proxy headers will be used. */
57
    const PROXY_HEADERS      = 2;
58
    /** Indicates any CloudFlare specific headers will be used. */
59
    const CLOUDFLARE_HEADERS = 4;
60
    /** Indicates any Incapsula specific headers will be used. */
61
    const INCAPSULA_HEADERS  = 8;
62
    /** Indicates custom listed headers will be used. */
63
    const CUSTOM_HEADERS     = 128;
64
65
    /** The array of mapped header strings. */
66
    private static $headers = array(
67
        self::CUSTOM_HEADERS     => array(),
68
        self::INCAPSULA_HEADERS  => array(
69
            'incap-client-ip'
70
        ),
71
        self::CLOUDFLARE_HEADERS => array(
72
            'cf-connecting-ip'
73
        ),
74
        self::PROXY_HEADERS      => array(
75
            'client-ip',
76
            'x-forwarded-for',
77
            'x-forwarded',
78
            'x-cluster-client-ip',
79
            'forwarded-for',
80
            'forwarded',
81
            'x-real-ip',
82
        ),
83
    );
84
85
    /** the bitmask of enabled methods */
86
    private $enabled;
87
88
    /** the array of IP whitelist ranges to check against */
89
    private $whitelist;
90
91
    /**
92
     * An object holding the source of addresses we will check
93
     *
94
     * @var RequestAdapter
95
     */
96
    private $source;
97
98
    /**
99
     * Constructor for the class.
100
     * @param int $enabled The bitmask of enabled headers.
101
     * @param array $whitelists The array of IP ranges to be whitelisted.
102
     * @param mixed $source A supported source of IP data.
103
     */
104 23
    public function __construct($enabled = self::ALL_METHODS, array $whitelists = array(), $source = null)
105
    {
106 23
        $this->enabled   = (int) $enabled;
107 23
        if (isset($source)) {
108 17
            $this->setSource($source);
109 16
        }
110 22
        $this->whitelist = array();
111 22
        foreach ($whitelists as $header => $ipRanges) {
112 12
            $header = $this->normalizeHeaderName($header);
113 12
            $this->whitelist[$header] = new IpWhitelist($ipRanges);
114 22
        }
115 22
    }
116
117
    /**
118
     * Adds a custom header to the list.
119
     * @param string $header The custom header to add.
120
     * @return Whip Returns $this.
121
     */
122 1
    public function addCustomHeader($header)
123
    {
124 1
        self::$headers[self::CUSTOM_HEADERS][] = $this->normalizeHeaderName($header);
125 1
        return $this;
126
    }
127
128
    /**
129
     * Sets the source data used to lookup the addresses.
130
     *
131
     * @param $source The source array.
132
     * @return Whip Returns $this.
133
     */
134 21
    public function setSource($source)
135
    {
136 21
        $this->source = $this->getRequestAdapter($source);
137
138 20
        return $this;
139
    }
140
141
    /**
142
     * Returns the IP address of the client using the given methods.
143
     * @param mixed $source (optional) The source data. If omitted, the class
144
     *        will use the value passed to Whip::setSource or fallback to
145
     *        $_SERVER.
146
     * @return string Returns the IP address as a string or false if no
147
     *         IP address could be found.
148
     */
149 22
    public function getIpAddress($source = null)
150
    {
151 22
        $source = $this->getRequestAdapter($this->coalesceSources($source));
152 22
        $remoteAddr = $source->getRemoteAddr();
153 22
        $requestHeaders = $source->getHeaders();
154
155 22
        foreach (self::$headers as $key => $headers) {
156 22
            if (!$this->isMethodUsable($key, $remoteAddr)) {
157 20
                continue;
158
            }
159
160 12
            if ($ipAddress = $this->extractAddressFromHeaders($requestHeaders, $headers)) {
161 9
                return $ipAddress;
162
            }
163 13
        }
164
165 13
        if ($remoteAddr && ($this->enabled & self::REMOTE_ADDR)) {
166 7
            return $remoteAddr;
167
        }
168
169 6
        return false;
170
    }
171
172
    /**
173
     * Returns the valid IP address or false if no valid IP address was found.
174
     * @param mixed $source (optional) The source data. If omitted, the class
175
     *        will use the value passed to Whip::setSource or fallback to
176
     *        $_SERVER.
177
     * @return string|false Returns the IP address (as a string) of the client or false
178
     *         if no valid IP address was found.
179
     */
180 4
    public function getValidIpAddress($source = null)
181
    {
182 4
        $ipAddress = $this->getIpAddress($source);
183 4
        if (false === $ipAddress || false === @inet_pton($ipAddress)) {
184 1
            return false;
185
        }
186 3
        return $ipAddress;
187
    }
188
189
    /**
190
     * Normalizes HTTP header name representations.
191
     *
192
     * HTTP_MY_HEADER and My-Header would be transformed to my-header.
193
     *
194
     * @param string $header The original header name.
195
     * @return string The normalized header name.
196
     */
197 12
    private function normalizeHeaderName($header)
198
    {
199 12
        if (strpos($header, 'HTTP_') === 0) {
200 1
            $header = str_replace('_', '-', substr($header, 5));
201 1
        }
202 12
        return strtolower($header);
203
    }
204
205
    /**
206
     * Finds the first element in $headers that is present in $_SERVER and
207
     * returns the IP address mapped to that value.
208
     * If the IP address is a list of comma separated values, the last value
209
     * in the list will be returned.
210
     * If no IP address is found, we return false.
211
     * @param array $requestHeaders The request headers to pull data from.
212
     * @param array $headers The list of headers to check.
213
     * @return string|false Returns the IP address as a string or false if no IP
214
     *         IP address was found.
215
     */
216 12
    private function extractAddressFromHeaders($requestHeaders, $headers)
217
    {
218 12
        foreach ($headers as $header) {
219 12
            if (!empty($requestHeaders[$header])) {
220 9
                $list = explode(',', $requestHeaders[$header]);
221 9
                return trim(end($list));
222
            }
223 11
        }
224 3
        return false;
225
    }
226
227
    /**
228
     * Returns whether or not the given method is enabled and usable.
229
     *
230
     * This method checks if the method is enabled and whether the method's data
231
     * is usable given it's IP whitelist.
232
     *
233
     * @param string $key The source key.
234
     * @param string $ipAddress The IP address.
235
     * @return boolean Returns true if the IP address is whitelisted and false
236
     *         otherwise. Returns true if the source does not have a whitelist
237
     *         specified.
238
     */
239 22
    private function isMethodUsable($key, $ipAddress)
240
    {
241 22
        if (!($key & $this->enabled)) {
242 20
            return false;
243
        }
244 16
        if (!isset($this->whitelist[$key])) {
245 4
            return true;
246
        }
247 12
        return $this->whitelist[$key]->isIpWhitelisted($ipAddress);
248
    }
249
250
    /**
251
     * Get a source/request adapter for a given source of IP data.
252
     *
253
     * @param mixed $source A supported source of request data.
254
     * @return RequestAdapter A RequestAdapter implementation for the given source.
255
     */
256 23
    private function getRequestAdapter($source)
257
    {
258 23
        if ($source instanceof RequestAdapter) {
259 20
            return $source;
260 23
        } elseif ($source instanceof ServerRequestInterface) {
261 1
            return new Psr7RequestAdapter($source);
262 22
        } elseif (is_array($source)) {
263 21
            return new SuperglobalRequestAdapter($source);
264
        }
265
266 1
        throw new \InvalidArgumentException("Unknown IP source.");
267
    }
268
269
    /**
270
     * Given available sources, get the first available source of IP data.
271
     *
272
     * @param mixed $source A source data argument, if available.
273
     * @return mixed The best available source, after fallbacks.
274
     */
275 22
    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...
276
    {
277 22
        if (isset($source)) {
278 1
            return $source;
279 22
        } elseif (isset($this->source)) {
280 20
            return $this->source;
281
        }
282
283 2
        return $_SERVER;
284
    }
285
}
286