Whip::addCustomHeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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