Completed
Push — master ( 256735...09813d )
by François
02:01
created

Bouncer::end()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
c 10
b 0
f 0
dl 0
loc 23
rs 8.7972
cc 4
eloc 12
nc 5
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
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
    public static $supportedOptions = array(
33
        'cache',
34
        'request',
35
        'logger',
36
        'profile',
37
        'cookieName',
38
        'cookiePath',
39
        'exitHandler',
40
        'responseCodeHandler'
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 $exitHandler;
74
75
    /**
76
     * The callable to use to set the HTTP Response Code
77
     *
78
     * @var callable
79
     */
80
    protected $responseCodeHandler;
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
    /**
130
     * Constructor.
131
     *
132
     * @param array $options
133
     */
134
    public function __construct(array $options = array())
135
    {
136
        if (!empty($options)) {
137
            $this->setOptions($options);
138
        }
139
140
        // Load Profile
141
        if (!$this->profile) {
142
            $this->profile = new \Bouncer\Profile\DefaultProfile;
143
        }
144
145
        call_user_func_array(array($this->profile, 'load'), array($this));
146
    }
147
148
    /*
149
     * Set the supported options
150
     */
151
    public function setOptions(array $options = array())
152
    {
153
        foreach (static::$supportedOptions as $key) {
154
            if (isset($options[$key])) {
155
                $this->$key = $options[$key];
156
            }
157
        }
158
    }
159
160
    /**
161
     * @throw Exception
162
     */
163
    public function error($message)
164
    {
165
        if ($this->throwExceptions) {
166
            throw new Exception($message);
167
        }
168
        if ($this->logErrors) {
169
            error_log("Bouncer: {$message}");
170
        }
171
    }
172
173
    /**
174
     * @return \Bouncer\Cache\CacheInterface
175
     */
176
    public function getCache($reportError = false)
177
    {
178
        if (empty($this->cache)) {
179
            if ($reportError) {
180
                $this->error('No cache available.');
181
            }
182
            return;
183
        }
184
185
        return $this->cache;
186
    }
187
188
    /**
189
     * @return \Bouncer\Logger\LoggerInterface
190
     */
191
    public function getLogger($reportError = false)
192
    {
193
        if (empty($this->logger)) {
194
            if ($reportError) {
195
                $this->error('No logger available.');
196
            }
197
            return;
198
        }
199
200
        return $this->logger;
201
    }
202
203
    /**
204
     * @return Request
205
     */
206
    public function getRequest()
207
    {
208
        if (isset($this->request)) {
209
            return $this->request;
210
        }
211
212
        $request = Request::createFromGlobals();
213
        $request->setTrustedProxies(array('127.0.0.1'));
214
        $request->setTrustedHeaderName(Request::HEADER_FORWARDED, null);
215
216
        return $this->request = $request;
217
    }
218
219
    /**
220
     * @return array
221
     */
222
    public function getResponse()
223
    {
224
        return $this->response;
225
    }
226
227
    /**
228
     * @return string
229
     */
230
    public function getUserAgent()
231
    {
232
        return $this->getRequest()->getUserAgent();
233
    }
234
235
    /**
236
     * @return string
237
     */
238
    public function getAddr()
239
    {
240
        return $this->getRequest()->getAddr();
241
    }
242
243
    /**
244
     * @return Address
245
     */
246
    public function getAddress()
247
    {
248
        $addr = $this->getRequest()->getAddr();
249
250
        $address = new Address($addr);
251
252
        return $address;
253
    }
254
255
    /**
256
     * @return array
257
     */
258
    public function getHeaders()
259
    {
260
        $request = $this->getRequest();
261
262
        $headers = $request->getHeaders();
263
264
        return $headers;
265
    }
266
267
    /**
268
     * @return Signature
269
     */
270
    public function getSignature()
271
    {
272
        $headers = $this->getHeaders();
273
274
        $signature = new Signature(array('headers' => $headers));
275
276
        return $signature;
277
    }
278
279
    /**
280
     * @return array
281
     */
282
    public function getCookies()
283
    {
284
        $names = array($this->cookieName, '__utmz', '__utma');
285
286
        $request = $this->getRequest();
287
288
        return $request->getCookies($names);
289
    }
290
291
    /**
292
     * Return the current session id (from Cookie)
293
     *
294
     * @return string|null
295
     */
296
    public function getSessionId()
297
    {
298
        $request = $this->getRequest();
299
300
        return $request->getCookie($this->cookieName);
301
    }
302
303
    /**
304
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
305
     *
306
     * @return string|null
307
     */
308
    public function getProtocol()
309
    {
310
        $request = $this->getRequest();
311
312
        return $request->getProtocol();
313
    }
314
315
    /**
316
     * @return Identity
317
     */
318
    public function getIdentity()
319
    {
320
        if (isset($this->identity)) {
321
            return $this->identity;
322
        }
323
324
        $cache = $this->getCache();
325
326
        $identity = new Identity(array(
327
            'address' => $this->getAddress(),
328
            'headers' => $this->getHeaders(),
329
            'session' => $this->getSessionId(),
330
        ));
331
332
        $id = $identity->getId();
333
334
        // Try to get Identity from cache
335
        if ($cache) {
336
            $cacheIdentity = $cache->getIdentity($id);
337
            if ($cacheIdentity instanceof Identity) {
338
                return $this->identity = $cacheIdentity;
339
            }
340
        }
341
342
        // Process Analyzers
343
        $identity = $this->processAnalyzers('identity', $identity);
344
345
        // Store Identity in cache
346
        if ($cache) {
347
            $cache->setIdentity($id, $identity);
348
        }
349
350
        return $this->identity = $identity;
351
    }
352
353
    /**
354
     * @return array
355
     */
356
    public function getContext()
357
    {
358
        if (!isset($this->context)) {
359
            $this->initContext();
360
        }
361
362
        return $this->context;
363
    }
364
365
    /*
366
     * Init the context with time and pid.
367
     */
368
    public function initContext()
369
    {
370
        $this->context = array();
371
372
        $this->addContext('time', microtime(true));
373
374
        $this->addContext('bouncer', array('pid' => getmypid()));
375
    }
376
377
    /*
378
     * @param string               $key
379
     * @param boolean|string|array $properties
380
     */
381
    public function addContext($key, $properties)
382
    {
383
        if (isset($this->context[$key]) && is_array($this->context[$key])) {
384
            $this->context[$key] = array_merge($this->context[$key], $properties);
385
        } else {
386
            $this->context[$key] = $properties;
387
        }
388
    }
389
390
    /*
391
     * Complete the context with session, exec_time and memory_usage.
392
     */
393
    public function completeContext()
394
    {
395
        $context = $this->getContext();
396
397
        // Session Id (from Cookie)
398
        $sessionId = $this->getSessionId();
399
        if (isset($sessionId)) {
400
            $this->addContext('session', $sessionId);
401
        }
402
403
        // Measure execution time
404
        $execution_time = round(microtime(true) - $context['time'], 4);
405
        if (!empty($context['bouncer']['throttle_time'])) {
406
            $execution_time -= $context['bouncer']['throttle_time'];
407
        }
408
409
        $this->addContext('bouncer', array(
410
            'execution_time' => $execution_time,
411
            'memory_usage'   => memory_get_peak_usage(),
412
        ));
413
    }
414
415
    /*
416
     * Complete the response with status code
417
     */
418
    public function completeResponse()
419
    {
420
        if (!isset($this->response)) {
421
            $this->response = array();
422
        }
423
424
        if (is_callable($this->responseCodeHandler)) {
425
            $responseCodeHandler = $this->responseCodeHandler;
426
            $responseStatus = $responseCodeHandler();
427
            if ($responseStatus) {
428
                $this->response['status'] = $responseStatus;
429
            }
430
        }
431
    }
432
    /*
433
     * Register an analyzer for a given type.
434
     *
435
     * @param string
436
     * @param callable
437
     * @param int
438
     */
439
    public function registerAnalyzer($type, $callable, $priority = 100)
440
    {
441
        $this->analyzers[$type][] = array($callable, $priority);
442
    }
443
444
    /*
445
     * Process Analyzers for a given type. Return the modified array or object.
446
     *
447
     * @param string
448
     * @param object
449
     *
450
     * @return object
451
     */
452
    protected function processAnalyzers($type, $value)
453
    {
454
        if (isset($this->analyzers[$type])) {
455
            // TODO: order analyzers by priority
456
            foreach ($this->analyzers[$type] as $array) {
457
                list($callable) = $array;
458
                $value = call_user_func_array($callable, array($value));
459
            }
460
        }
461
        return $value;
462
    }
463
464
    /*
465
     * Start a Bouncer session
466
     */
467
    public function start()
468
    {
469
        // Already started, skip
470
        if ($this->started === true) {
471
            return;
472
        }
473
474
        $this->initContext();
475
476
        $this->initSession();
477
478
        register_shutdown_function(array($this, 'end'));
479
480
        $this->started = true;
481
    }
482
483
    /*
484
     * Set a cookie containing the session id
485
     */
486
    public function initSession()
487
    {
488
        $identity = $this->getIdentity();
489
490
        $identitySession = $identity->getSession();
491
        if ($identitySession) {
492
            $curentSessionId = $this->getSessionId();
493
            $identitySessionId = $identitySession->getId();
494
            if (empty($curentSessionId) || $curentSessionId !== $identitySessionId) {
495
                setcookie($this->cookieName, $identitySessionId, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
496
            }
497
        }
498
    }
499
500
    /*
501
     * Throttle
502
     *
503
     * @param int   $minimum in milliseconds
504
     * @param int   $maximum in milliseconds
505
     *
506
     */
507
    public function throttle($minimum = 1000, $maximum = 2500)
508
    {
509
        // In microseconds
510
        $throttleTime = rand($minimum * 1000, $maximum * 1000);
511
        usleep($throttleTime);
512
513
        // In seconds
514
        $this->addContext('bouncer', array('throttle_time' => $throttleTime));
515
    }
516
517
    /*
518
     * @deprecated deprecated since version 2.1.0
519
     */
520
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
521
    {
522
        $identity = $this->getIdentity();
523
524
        if (in_array($identity->getStatus(), $statuses)) {
525
            return $this->throttle($minimum, $maximum);
526
        }
527
    }
528
529
    /*
530
     * Block
531
     *
532
     * @param string $type
533
     * @param array  $extra
534
     *
535
     */
536
    public function block($type = null, $extra = null)
537
    {
538
        $this->addContext('blocked', true);
539
540
        if (isset($type)) {
541
            $this->registerEvent($type, $extra);
542
        }
543
544
        if (is_callable($this->responseCodeHandler)) {
545
            $responseCodeHandler = $this->responseCodeHandler;
546
            $responseCodeHandler(403, 'Forbidden');
547
        }
548
        else {
549
            $this->error('No response code handler available.');
550
        }
551
552
        if (is_callable($this->exitHandler)) {
553
            $exitHandler = $this->exitHandler;
554
            $exitHandler();
555
        }
556
        else {
557
            // $this->error('No exit callable set. PHP exit construct will be used.');
558
            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...
559
        }
560
    }
561
562
    /*
563
     * @deprecated deprecated since version 2.1.0
564
     */
565
    public function ban($statuses = array())
566
    {
567
        $identity = $this->getIdentity();
568
569
        if (in_array($identity->getStatus(), $statuses)) {
570
            return $this->block();
571
        }
572
573
    }
574
575
    /*
576
     * @param string $type
577
     * @param array  $extra
578
     */
579
    public function registerEvent($type, $extra = null)
580
    {
581
        $this->context['event']['type'] = $type;
582
        if (!empty($extra)) {
583
            $this->context['event']['extra'] = $extra;
584
        }
585
    }
586
587
    /*
588
     * Complete the connection then attempt to log.
589
     */
590
    public function end()
591
    {
592
        // Already ended, skip
593
        if ($this->ended === true) {
594
            return;
595
        }
596
597
        $this->completeContext();
598
        $this->completeResponse();
599
600
        if (function_exists('fastcgi_finish_request')) {
601
            fastcgi_finish_request();
602
        }
603
604
        // We really want to avoid throwing exceptions there
605
        try {
606
            $this->log();
607
        } catch (Exception $e) {
608
            error_log($e->getMessage());
609
        }
610
611
        $this->ended = true;
612
    }
613
614
    /*
615
     * Log the connection to the logging backend.
616
     */
617
    public function log()
618
    {
619
        $logEntry = array(
620
            'address'  => $this->getAddress(),
621
            'request'  => $this->getRequest(),
622
            'response' => $this->getResponse(),
623
            'identity' => $this->getIdentity(),
624
            'session'  => $this->getSessionId(),
625
            'context'  => $this->getContext(),
626
        );
627
628
        $logger = $this->getLogger();
629
        if ($logger) {
630
            $logger->log($logEntry);
631
        }
632
    }
633
634
    // Static
635
636
    public static function hash($value)
637
    {
638
        return md5($value);
639
    }
640
641
}
642