Completed
Push — master ( 12a557...496eb3 )
by François
02:35
created

Bouncer::block()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 2 Features 2
Metric Value
c 3
b 2
f 2
dl 0
loc 25
rs 8.5806
cc 4
eloc 14
nc 8
nop 2
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
use Bouncer\Resource\Address;
15
use Bouncer\Resource\Identity;
16
17
class Bouncer
18
{
19
20
    const NICE       = 'nice';
21
    const OK         = 'ok';
22
    const SUSPICIOUS = 'suspicious';
23
    const BAD        = 'bad';
24
25
    const ROBOT      = 'robot';
26
    const BROWSER    = 'browser';
27
    const UNKNOWN    = 'unknown';
28
29
    /**
30
     * @var array
31
     */
32
    static $supportedOptions = array(
33
        'cache',
34
        'request',
35
        'logger',
36
        'profile',
37
        'cookieName',
38
        'cookiePath',
39
        'exit',
40
        'responseCodeSetter'
41
    );
42
43
    /**
44
     * @var string|object
45
     */
46
    protected $profile;
47
48
    /**
49
     * @var boolean
50
     */
51
    protected $throwExceptions = false;
52
53
    /**
54
     * @var boolean
55
     */
56
    protected $logErrors = true;
57
58
    /**
59
     * @var string
60
     */
61
    protected $cookieName = 'bsid';
62
63
    /**
64
     * @var string
65
     */
66
    protected $cookiePath = '/';
67
68
    /**
69
     * The exit callable to use when blocking a request
70
     *
71
     * @var callable
72
     */
73
    protected $exit;
74
75
    /**
76
     * The callable to use to set the HTTP Response Code
77
     *
78
     * @var callable
79
     */
80
    protected $responseCodeSetter = 'http_response_code';
81
82
    /**
83
     * @var \Bouncer\Cache\CacheInterface
84
     */
85
    protected $cache;
86
87
    /**
88
     * @var \Bouncer\Logger\LoggerInterface
89
     */
90
    protected $logger;
91
92
    /**
93
     * @var Request
94
     */
95
    protected $request;
96
97
    /**
98
     * @var array
99
     */
100
    protected $response;
101
102
    /**
103
     * @var array
104
     */
105
    protected $analyzers = array();
106
107
    /**
108
     * @var Identity
109
     */
110
    protected $identity;
111
112
    /**
113
     * Store internal metadata
114
     *
115
     * @var array
116
     */
117
    protected $context;
118
119
    /**
120
     * @var boolean
121
     */
122
    protected $started = false;
123
124
    /**
125
     * @var boolean
126
     */
127
    protected $ended = false;
128
129
    public function __construct(array $options = array())
130
    {
131
        if (!empty($options)) {
132
            $this->setOptions($options);
133
        }
134
135
        // Load Profile
136
        if (!$this->profile) {
137
            $this->profile = new \Bouncer\Profile\DefaultProfile;
138
        }
139
140
        call_user_func_array(array($this->profile, 'load'), array($this));
141
    }
142
143
    /*
144
     * Set the supported options
145
     */
146
    public function setOptions(array $options = array())
147
    {
148
        foreach (static::$supportedOptions as $key) {
0 ignored issues
show
Bug introduced by
Since $supportedOptions is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $supportedOptions to at least protected.

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

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

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 { }

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

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

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
149
            if (isset($options[$key])) {
150
                $this->$key = $options[$key];
151
            }
152
        }
153
    }
154
155
    /**
156
     * @throw Exception
157
     */
158
    public function error($message)
159
    {
160
        if ($this->throwExceptions) {
161
            throw new Exception($message);
162
        }
163
        if ($this->logErrors) {
164
            error_log("Bouncer: {$message}");
165
        }
166
    }
167
168
    /**
169
     * @return \Bouncer\Cache\CacheInterface
170
     */
171
    public function getCache($reportError = false)
172
    {
173
        if (empty($this->cache)) {
174
            if ($reportError) {
175
                $this->error('No cache available.');
176
            }
177
            return;
178
        }
179
180
        return $this->cache;
181
    }
182
183
    /**
184
     * @return \Bouncer\Logger\LoggerInterface
185
     */
186
    public function getLogger($reportError = false)
187
    {
188
        if (empty($this->logger)) {
189
            if ($reportError) {
190
                $this->error('No logger available.');
191
            }
192
            return;
193
        }
194
195
        return $this->logger;
196
    }
197
198
    /**
199
     * @return Request
200
     */
201
    public function getRequest()
202
    {
203
        if (isset($this->request)) {
204
            return $this->request;
205
        }
206
207
        $request = Request::createFromGlobals();
208
209
        return $this->request = $request;
210
    }
211
212
    /**
213
     * @return array
214
     */
215
    public function getResponse()
216
    {
217
        return $this->response;
218
    }
219
220
    /**
221
     * @return string
222
     */
223
    public function getUserAgent()
224
    {
225
        return $this->getRequest()->getUserAgent();
226
    }
227
228
    /**
229
     * @return string
230
     */
231
    public function getAddr()
232
    {
233
        return $this->getRequest()->getAddr();
234
    }
235
236
    /**
237
     * @return Address
238
     */
239
    public function getAddress()
240
    {
241
        $addr = $this->getRequest()->getAddr();
242
243
        $address = new Address($addr);
244
245
        return $address;
246
    }
247
248
    /**
249
     * @return array
250
     */
251
    public function getHeaders()
252
    {
253
        $request = $this->getRequest();
254
255
        $headers = $request->getHeaders();
256
257
        return $headers;
258
    }
259
260
    /**
261
     * @return Signature
262
     */
263
    public function getSignature()
264
    {
265
        $headers = $this->getHeaders();
266
267
        $signature = new Signature(array('headers' => $headers));
268
269
        return $signature;
270
    }
271
272
    /**
273
     * @return array
274
     */
275
    public function getCookies()
276
    {
277
        $names = array($this->cookieName, '__utmz', '__utma');
278
279
        $request = $this->getRequest();
280
281
        return $request->getCookies($names);
282
    }
283
284
    /**
285
     * Return the current session id (from Cookie)
286
     *
287
     * @return string|null
288
     */
289
    public function getSessionId()
290
    {
291
        $request = $this->getRequest();
292
293
        return $request->getCookie($this->cookieName);
294
    }
295
296
    /**
297
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
298
     *
299
     * @return string|null
300
     */
301
    public function getProtocol()
302
    {
303
        $request = $this->getRequest();
304
305
        return $request->getProtocol();
306
    }
307
308
    /**
309
     * @return Identity
310
     */
311
    public function getIdentity()
312
    {
313
        if (isset($this->identity)) {
314
            return $this->identity;
315
        }
316
317
        $cache = $this->getCache();
318
319
        $identity = new Identity(array(
320
            'address' => $this->getAddress(),
321
            'headers' => $this->getHeaders(),
322
        ));
323
324
        $id = $identity->getId();
325
326
        // Try to get Identity from cache
327
        if ($cache) {
328
            $cacheIdentity = $cache->getIdentity($id);
329
            if ($cacheIdentity instanceof Identity) {
330
                return $this->identity = $cacheIdentity;
331
            }
332
        }
333
334
        // Process Analyzers
335
        $identity = $this->processAnalyzers('identity', $identity);
336
337
        // Store Identity in cache
338
        if ($cache) {
339
            $cache->setIdentity($id, $identity);
340
        }
341
342
        return $this->identity = $identity;
343
    }
344
345
    public function getContext()
346
    {
347
        if (!isset($this->context)) {
348
            $this->initContext();
349
        }
350
351
        return $this->context;
352
    }
353
354
    /*
355
     * Init the context with id, time and start.
356
     */
357
    public function initContext()
358
    {
359
        $this->context = array();
360
        $this->context['pid']   = getmypid();
361
        $this->context['time']  = time();
362
        $this->context['start'] = microtime(true);
363
    }
364
365
    /*
366
     * Complete the context with end, exec_time and memory_usage.
367
     */
368
    public function completeContext()
369
    {
370
        // Session Id (from Cookie)
371
        $sessionId = $this->getSessionId();
372
        if (isset($sessionId)) {
373
            $this->context['session'] = $sessionId;
374
        }
375
376
        // Measure execution time
377
        $this->context['end'] = microtime(true);
378
        $this->context['exec_time'] = round($this->context['end'] - $this->context['start'], 4);
379
        if (!empty($this->context['throttle_time'])) {
380
             $this->context['exec_time'] -= $this->context['throttle_time'];
381
        }
382
        unset($this->context['end'], $this->context['start']);
383
384
        // Report Memory Usage
385
        $this->context['memory_usage'] = memory_get_peak_usage();
386
    }
387
388
    /*
389
     * Complete the response with status code
390
     */
391
    public function completeResponse()
392
    {
393
        if (!isset($this->response)) {
394
            $this->response = array();
395
        }
396
397
        if (function_exists('http_response_code')) {
398
            $responseStatus = http_response_code();
399
            if ($responseStatus) {
400
                $this->response['status'] = $responseStatus;
401
            }
402
        }
403
    }
404
    /*
405
     * Register an analyzer for a given type.
406
     *
407
     * @param string
408
     * @param callable
409
     * @param int
410
     */
411
    public function registerAnalyzer($type, $callable, $priority = 100)
412
    {
413
        $this->analyzers[$type][] = array($callable, $priority);
414
    }
415
416
    /*
417
     * Process Analyzers for a given type. Return the modified array or object.
418
     *
419
     * @param string
420
     * @param object
421
     *
422
     * @return object
423
     */
424
    protected function processAnalyzers($type, $value)
425
    {
426
        if (isset($this->analyzers[$type])) {
427
            // TODO: order analyzers by priority
428
            foreach ($this->analyzers[$type] as $array) {
429
                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...
430
                $value = call_user_func_array($callable, array($value));
431
            }
432
        }
433
        return $value;
434
    }
435
436
    /*
437
     * Start a Bouncer session
438
     */
439
    public function start()
440
    {
441
        // Already started, skip
442
        if ($this->started === true) {
443
            return;
444
        }
445
446
        $this->initContext();
447
448
        $this->initSession();
449
450
        register_shutdown_function(array($this, 'end'));
451
452
        $this->started = true;
453
    }
454
455
    /*
456
     * Set a cookie containing the session id
457
     */
458
    public function initSession()
459
    {
460
        $identity = $this->getIdentity();
461
462
        $identitySession = $identity->getSession();
463
        if ($identitySession) {
464
            $curentSessionId = $this->getSessionId();
465
            $identitySessionId = $identitySession->getId();
466
            if (empty($curentSessionId) || $curentSessionId !== $identitySessionId) {
467
                setcookie($this->cookieName, $identitySessionId, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
468
            }
469
        }
470
    }
471
472
    /*
473
     * Throttle
474
     *
475
     * @param array $statuses
0 ignored issues
show
Bug introduced by
There is no parameter named $statuses. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
476
     * @param int   $minimum
477
     * @param int   $maximum
478
     *
479
     */
480
    public function throttle($minimum = 1000, $maximum = 2500)
481
    {
482
        $throttleTime = rand($minimum * 1000, $maximum * 1000);
483
        usleep($throttleTime);
484
        $this->context['throttle_time'] = round($throttleTime / 1000 / 1000, 3);
485
    }
486
487
    /*
488
     * @deprecated deprecated since version 2.1.0
489
     */
490
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
491
    {
492
        $identity = $this->getIdentity();
493
494
        if (in_array($identity->getStatus(), $statuses)) {
495
            return $this->throttle($minimum, $maximum);
496
        }
497
    }
498
499
    /*
500
     * Block
501
     *
502
     * @param string $type
503
     * @param array  $extra
504
     *
505
     */
506
    public function block($type = null, $extra = null)
507
    {
508
        $this->context['blocked'] = true;
509
510
        if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type 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...
511
            $this->registerEvent($type, $extra);
512
        }
513
514
        if (is_callable($this->responseCodeSetter)) {
515
            $responseCodeSetter = $this->responseCodeSetter;
516
            $responseCodeSetter('503', 'Service Unavailable');
517
        }
518
        else {
519
            $this->error('No response code setter available.');
520
        }
521
522
        if (is_callable($this->exit)) {
523
            $callable = $this->exit;
524
            $callable();
525
        }
526
        else {
527
            // $this->error('No exit callable set. PHP exit construct will be used.');
528
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method block() 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...
529
        }
530
    }
531
532
    /*
533
     * @deprecated deprecated since version 2.1.0
534
     */
535
    public function ban($statuses = array())
536
    {
537
        $identity = $this->getIdentity();
538
539
        if (in_array($identity->getStatus(), $statuses)) {
540
            $this->context['banned'] = true;
541
            return $this->block();
542
        }
543
544
    }
545
546
    /*
547
     * @param string $type
548
     * @param array  $extra
549
     */
550
    public function registerEvent($type, $extra = null)
551
    {
552
        $this->context['event']['type'] = $type;
553
        if (!empty($extra)) {
554
            $this->context['event']['extra'] = $extra;
555
        }
556
    }
557
558
    /*
559
     * Complete the connection then attempt to log.
560
     */
561
    public function end()
562
    {
563
        // Already ended, skip
564
        if ($this->ended === true) {
565
            return;
566
        }
567
568
        $this->completeContext();
569
        $this->completeResponse();
570
571
        // We really want to avoid throwing exceptions there
572
        try {
573
            $this->log();
574
        } catch (Exception $e) {
575
            error_log($e->getMessage());
576
        }
577
578
        $this->ended = true;
579
    }
580
581
    /*
582
     * Log the connection to the logging backend.
583
     */
584
    public function log()
585
    {
586
        $logEntry = array(
587
            'address'  => $this->getAddress(),
588
            'request'  => $this->getRequest(),
589
            'response' => $this->getResponse(),
590
            'identity' => $this->getIdentity(),
591
            'context'  => $this->getContext(),
592
        );
593
594
        $logger = $this->getLogger();
595
        if ($logger) {
596
            $logger->log($logEntry);
597
        }
598
    }
599
600
    // Static
601
602
    public static function hash($value)
603
    {
604
        return md5($value);
605
    }
606
607
}
608