Completed
Push — next ( 607f27...9ea627 )
by François
02:07
created

Bouncer::getRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 10
rs 9.4286
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Bouncer package.
5
 *
6
 * (c) François Hodierne <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Bouncer;
13
14
class Bouncer
15
{
16
17
    const NICE       = 'nice';
18
    const OK         = 'ok';
19
    const SUSPICIOUS = 'suspicious';
20
    const BAD        = 'bad';
21
22
    const ROBOT      = 'robot';
23
    const BROWSER    = 'browser';
24
    const UNKNOWN    = 'unknown';
25
26
    /**
27
     * @var string|object
28
     */
29
    protected $profile;
30
31
    /**
32
     * @var boolean
33
     */
34
    protected $throwExceptions = false;
35
36
    /**
37
     * @var boolean
38
     */
39
    protected $logErrors = true;
40
41
    /**
42
     * @var boolean
43
     */
44
    protected $cookieName = 'bsid';
45
46
    /**
47
     * @var \Bouncer\Cache\CacheInterface
48
     */
49
    protected $cache;
50
51
    /**
52
     * @var \Bouncer\Logger\LoggerInterface
53
     */
54
    protected $logger;
55
56
    /**
57
     * @var Request
58
     */
59
    protected $request;
60
61
    /**
62
     * @var array
63
     */
64
    protected $analyzers = array();
65
66
    /**
67
     * @var Identity
68
     */
69
    protected $identity;
70
71
    /**
72
     * Store metadata about the handling of the request
73
     *
74
     * @var array
75
     */
76
    protected $connection = array();
77
78
    /**
79
     * @var boolean
80
     */
81
    protected $started = false;
82
83
    /**
84
     * @var boolean
85
     */
86
    protected $ended = false;
87
88
    public function __construct(array $options = array())
89
    {
90
        if (!empty($options)) {
91
            $this->setOptions($options);
92
        }
93
94
        // Load Profile
95
        if (!$this->profile) {
96
            $this->profile = new \Bouncer\Profile\Standard;
97
        }
98
        call_user_func_array(array($this->profile, 'load'), array($this));
99
    }
100
101
    /*
102
     * Set the supported options
103
     */
104
    public function setOptions(array $options = array())
105
    {
106
        if (isset($options['cache'])) {
107
            $this->cache = $options['cache'];
108
        }
109
        if (isset($options['request'])) {
110
            $this->request = $options['request'];
111
        }
112
        if (isset($options['logger'])) {
113
            $this->logger = $options['logger'];
114
        }
115
        if (isset($options['profile'])) {
116
            $this->profile = $options['profile'];
117
        }
118
    }
119
120
    /**
121
     * @throw Exception
122
     */
123
    public function error($message)
124
    {
125
        if ($this->throwExceptions) {
126
            throw new Exception($message);
127
        }
128
        if ($this->logErrors) {
129
            error_log("Bouncer: {$message}");
130
        }
131
    }
132
133
    /**
134
     * @return \Bouncer\Cache\CacheInterface
135
     */
136
    public function getCache($reportError = false)
137
    {
138
        if (empty($this->cache)) {
139
            if ($reportError) {
140
                $this->error('No cache available.');
141
            }
142
            return;
143
        }
144
145
        return $this->cache;
146
    }
147
148
    /**
149
     * @return \Bouncer\Logger\LoggerInterface
150
     */
151
    public function getLogger($reportError = false)
152
    {
153
        if (empty($this->logger)) {
154
            if ($reportError) {
155
                $this->error('No logger available.');
156
            }
157
            return;
158
        }
159
160
        return $this->logger;
161
    }
162
163
    /**
164
     * @return Request
165
     */
166
    public function getRequest()
167
    {
168
        if (isset($this->request)) {
169
            return $this->request;
170
        }
171
172
        $request = Request::createFromGlobals();
173
174
        return $this->request = $request;
175
    }
176
177
    /**
178
     * @return string
179
     */
180
    public function getUserAgent()
181
    {
182
        return $this->getRequest()->getUserAgent();
183
    }
184
185
    /**
186
     * @return string
187
     */
188
    public function getAddr()
189
    {
190
        return $this->getRequest()->getAddr();
191
    }
192
193
    /**
194
     * @return array
195
     */
196
    public function getHeaders()
197
    {
198
        $request = $this->getRequest();
199
200
        $headers = $request->getHeaders();
201
202
        // TODO: this should be deprecated
203
        $protocol = $this->getProtocol();
204
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
205
            $headers['protocol'] = $protocol;
206
        }
207
208
        return $headers;
209
    }
210
211
    /**
212
     * @return array
213
     */
214
    public function getCookies()
215
    {
216
        $names = [$this->cookieName, '__utmz', '__utma'];
217
218
        $request = $this->getRequest();
219
220
        return $request->getCookies($names);
221
    }
222
223
    /**
224
     * Return the current session id
225
     *
226
     * @return string|null
227
     */
228
    public function getSession()
229
    {
230
        $request = $this->getRequest();
231
232
        return $request->getCookie($this->cookieName);
233
    }
234
235
    /**
236
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
237
     *
238
     * @return string|null
239
     */
240
    public function getProtocol()
241
    {
242
        $request = $this->getRequest();
243
244
        return $request->getProtocol();
245
    }
246
247
    /**
248
     * @return Identity
249
     */
250
    public function getIdentity()
251
    {
252
        if (isset($this->identity)) {
253
            return $this->identity;
254
        }
255
256
        $cache = $this->getCache();
257
258
        $addr  = $this->getAddr();
259
        $haddr = self::hash($addr);
260
261
        $headers = $this->getHeaders();
262
        $fingerprint = Fingerprint::generate($headers);
263
264
        $id = self::hash($fingerprint . $haddr);
265
266
        // Try to get identity from cache
267
        if ($cache) {
268
            $identity = $cache->getIdentity($id);
269
            if ($identity) {
270
                return $this->identity = new Identity($identity);
271
            }
272
        }
273
274
        // Build base identity
275
        $identity = array(
276
            'id'          => $id,
277
            'addr'        => $addr,
278
            'haddr'       => $haddr,
279
            'headers'     => $headers,
280
            'fingerprint' => $fingerprint
281
        );
282
283
        // Extra identity (optional)
284
        $protocol = $this->getProtocol();
285
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
286
            $identity['protocol'] = $protocol;
287
        }
288
        $cookies = $this->getCookies();
289
        if ($cookies) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
290
            $identity['cookies'] = $cookies;
291
        }
292
        $session = $this->getSession();
293
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
294
            $identity['session'] = $session;
295
        }
296
297
        // Process Analyzers
298
        $identity = $this->processAnalyzers('identity', $identity);
299
300
        // Store Identity in cache
301
        if ($cache) {
302
            $cache->setIdentity($id, $identity);
303
        }
304
305
        return $this->identity = new Identity($identity);
0 ignored issues
show
Bug introduced by
It seems like $identity defined by $this->processAnalyzers('identity', $identity) on line 298 can also be of type object; however, Bouncer\Identity::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
306
    }
307
308
    public function getConnection()
309
    {
310
        if (!$this->connection) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->connection of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
311
            $this->initConnection();
312
        }
313
314
        return $this->connection;
315
    }
316
317
    /*
318
     * Init the connection with id, time and start.
319
     */
320
    public function initConnection()
321
    {
322
        $this->connection = array();
323
        $this->connection['pid']   = getmypid();
324
        $this->connection['time']  = time();
325
        $this->connection['start'] = microtime(true);
326
    }
327
328
    /*
329
     * Complete the connection with end, exec_time, memory_usage and response_status.
330
     */
331
    public function completeConnection()
332
    {
333
        // Session (from Cookie)
334
        $session = $this->getSession();
335
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
336
            $this->connection['session'] = $session;
337
        }
338
339
        // Measure execution time
340
        $this->connection['end'] = microtime(true);
341
        $this->connection['exec_time'] = round($this->connection['end'] - $this->connection['start'], 4);
342
        if (!empty($this->connection['throttle_time'])) {
343
             $this->connection['exec_time'] -= $this->connection['throttle_time'];
344
        }
345
        unset($this->connection['end'], $this->connection['start']);
346
347
        // Report Memory Usage
348
        $this->connection['memory_usage'] = memory_get_peak_usage();
349
350
        // Add response
351
        $responseStatus = http_response_code();
352
        if ($responseStatus) {
353
            $this->connection['response']['status'] = $responseStatus;
354
        }
355
    }
356
357
    /*
358
     * Register an analyzer for a given type.
359
     *
360
     * @param string
361
     * @param callable
362
     * @param int
363
     */
364
    public function registerAnalyzer($type, callable $callable, $priority = 100)
365
    {
366
        $this->analyzers[$type][] = array($callable, $priority);
367
    }
368
369
    /*
370
     * Process Analyzers for a given type. Return the modified array or object.
371
     *
372
     * @param string
373
     * @param array|object
374
     *
375
     * @return array|object
376
     */
377
    protected function processAnalyzers($type, $value)
378
    {
379
        if (isset($this->analyzers[$type])) {
380
            // TODO: order analyzers by priority
381
            foreach ($this->analyzers[$type] as $array) {
382
                list($callable, $priority) = $array;
0 ignored issues
show
Unused Code introduced by
The assignment to $priority is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
383
                $value = call_user_func_array($callable, array($value));
384
            }
385
        }
386
        return $value;
387
    }
388
389
    /*
390
     * Start a Bouncer session
391
     */
392
    public function start()
393
    {
394
        // Already started, skip
395
        if ($this->started === true) {
396
            return;
397
        }
398
399
        $this->initConnection();
400
401
        $this->initSession();
402
403
        register_shutdown_function([$this, 'end']);
404
405
        $this->started = true;
406
    }
407
408
    /*
409
     * Set a cookie containing the session id
410
     */
411
    public function initSession()
412
    {
413
        $identity = $this->getIdentity();
414
415
        if ($identity->hasAttribute('session')) {
416
            $curentSession = $this->getSession();
417
            $identitySession = $identity->getAttribute('session');
418
            if (empty($curentSession) || $curentSession !== $identitySession) {
419
                setcookie($this->cookieName, $identitySession, time() + (60 * 60 * 365 * 2), '/');
420
            }
421
        }
422
    }
423
424
    /*
425
     * Throttle if Identity status is suspicious.
426
     */
427
    public function throttle()
428
    {
429
        $identity = $this->getIdentity();
430
431
        $status = $identity->getStatus();
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
432
433
        switch ($status) {
434
            case self::BAD:
435
                // sleep 1 to 5 seconds then exit
436
                $throttle = rand(1000*1000, 5000*1000);
437
                usleep($throttle);
438
                $this->connection['throttle_time'] = round($throttle / 1000000, 3);
439
                break;
440
            case self::SUSPICIOUS:
441
                // sleep 0.5 to 2.5 seconds then continue
442
                $throttle = rand(500*1000, 2500*1000);
443
                usleep($throttle);
444
                $this->connection['throttle_time'] = round($throttle / 1000000, 3);
445
                break;
446
        }
447
    }
448
449
    /*
450
     * Ban if Identity status is bad.
451
     */
452
    public function ban()
453
    {
454
        $identity = $this->getIdentity();
455
456
        if ($identity->getStatus() == self::BAD) {
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
457
            $this->unavailable();
458
        }
459
    }
460
461
    /*
462
     * Complete the connection then attempt to log.
463
     */
464
    public function end()
465
    {
466
        // Already ended, skip
467
        if ($this->ended === true) {
468
            return;
469
        }
470
471
        $this->completeConnection();
472
473
        // We really want to avoid throwing exceptions there
474
        try {
475
            $this->log();
476
        } catch (Exception $e) {
477
            error_log($e->getMessage());
478
        }
479
480
        $this->ended = true;
481
    }
482
483
    /*
484
     * Log the connection to the logging backend.
485
     */
486
    public function log()
487
    {
488
        $connection = $this->getConnection();
489
        $identity = $this->getIdentity();
490
        $request = $this->getRequest();
491
492
        $logger = $this->getLogger();
493
        if ($logger) {
494
            $logger->log($connection, $identity, $request);
495
        }
496
    }
497
498
    // Static
499
500
    public static function unavailable()
501
    {
502
        $code = '503';
503
        $msg = 'Service Unavailable';
504
        header("HTTP/1.0 $code $msg");
505
        header("Status: $code $msg");
506
        echo $msg;
507
        exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method unavailable() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
508
    }
509
510
    public static function hash($string)
511
    {
512
        return md5($string);
513
    }
514
515
}
516