Completed
Push — master ( bf7eff...772dda )
by François
02:48
created

Bouncer::getLogger()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
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
    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) {
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
            'session' => $this->getSessionId(),
323
        ));
324
325
        $id = $identity->getId();
326
327
        // Try to get Identity from cache
328
        if ($cache) {
329
            $cacheIdentity = $cache->getIdentity($id);
330
            if ($cacheIdentity instanceof Identity) {
331
                return $this->identity = $cacheIdentity;
332
            }
333
        }
334
335
        // Process Analyzers
336
        $identity = $this->processAnalyzers('identity', $identity);
337
338
        // Store Identity in cache
339
        if ($cache) {
340
            $cache->setIdentity($id, $identity);
341
        }
342
343
        return $this->identity = $identity;
344
    }
345
346
    /**
347
     * @return array
348
     */
349
    public function getContext()
350
    {
351
        if (!isset($this->context)) {
352
            $this->initContext();
353
        }
354
355
        return $this->context;
356
    }
357
358
    /*
359
     * Init the context with id, time and start.
360
     */
361
    public function initContext()
362
    {
363
        $this->context = array();
364
        $this->context['pid']   = getmypid();
365
        $this->context['time']  = time();
366
        $this->context['start'] = microtime(true);
367
    }
368
369
    /*
370
     * @param string $key
371
     * @param array  $properties
372
     */
373
    public function addContext($key, $properties)
374
    {
375
        if (isset($this->context[$key]) && is_array($this->context[$key])) {
376
            $this->context[$key] = array_merge($this->context[$key], $properties);
377
        } else {
378
            $this->context[$key] = $properties;
379
        }
380
    }
381
382
    /*
383
     * Complete the context with end, exec_time and memory_usage.
384
     */
385
    public function completeContext()
386
    {
387
        // Session Id (from Cookie)
388
        $sessionId = $this->getSessionId();
389
        if (isset($sessionId)) {
390
            $this->context['session'] = $sessionId;
391
        }
392
393
        // Measure execution time
394
        $this->context['end'] = microtime(true);
395
        $this->context['exec_time'] = round($this->context['end'] - $this->context['start'], 4);
396
        if (!empty($this->context['throttle_time'])) {
397
             $this->context['exec_time'] -= $this->context['throttle_time'];
398
        }
399
        unset($this->context['end'], $this->context['start']);
400
401
        // Report Memory Usage
402
        $this->context['memory_usage'] = memory_get_peak_usage();
403
    }
404
405
    /*
406
     * Complete the response with status code
407
     */
408
    public function completeResponse()
409
    {
410
        if (!isset($this->response)) {
411
            $this->response = array();
412
        }
413
414
        if (is_callable($this->responseCodeHandler)) {
415
            $responseCodeHandler = $this->responseCodeHandler;
416
            $responseStatus = $responseCodeHandler();
417
            if ($responseStatus) {
418
                $this->response['status'] = $responseStatus;
419
            }
420
        }
421
    }
422
    /*
423
     * Register an analyzer for a given type.
424
     *
425
     * @param string
426
     * @param callable
427
     * @param int
428
     */
429
    public function registerAnalyzer($type, $callable, $priority = 100)
430
    {
431
        $this->analyzers[$type][] = array($callable, $priority);
432
    }
433
434
    /*
435
     * Process Analyzers for a given type. Return the modified array or object.
436
     *
437
     * @param string
438
     * @param object
439
     *
440
     * @return object
441
     */
442
    protected function processAnalyzers($type, $value)
443
    {
444
        if (isset($this->analyzers[$type])) {
445
            // TODO: order analyzers by priority
446
            foreach ($this->analyzers[$type] as $array) {
447
                list($callable) = $array;
448
                $value = call_user_func_array($callable, array($value));
449
            }
450
        }
451
        return $value;
452
    }
453
454
    /*
455
     * Start a Bouncer session
456
     */
457
    public function start()
458
    {
459
        // Already started, skip
460
        if ($this->started === true) {
461
            return;
462
        }
463
464
        $this->initContext();
465
466
        $this->initSession();
467
468
        register_shutdown_function(array($this, 'end'));
469
470
        $this->started = true;
471
    }
472
473
    /*
474
     * Set a cookie containing the session id
475
     */
476
    public function initSession()
477
    {
478
        $identity = $this->getIdentity();
479
480
        $identitySession = $identity->getSession();
481
        if ($identitySession) {
482
            $curentSessionId = $this->getSessionId();
483
            $identitySessionId = $identitySession->getId();
484
            if (empty($curentSessionId) || $curentSessionId !== $identitySessionId) {
485
                setcookie($this->cookieName, $identitySessionId, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
486
            }
487
        }
488
    }
489
490
    /*
491
     * Throttle
492
     *
493
     * @param int   $minimum
494
     * @param int   $maximum
495
     *
496
     */
497
    public function throttle($minimum = 1000, $maximum = 2500)
498
    {
499
        $throttleTime = rand($minimum * 1000, $maximum * 1000);
500
        usleep($throttleTime);
501
        $this->context['throttle_time'] = round($throttleTime / 1000 / 1000, 3);
502
    }
503
504
    /*
505
     * @deprecated deprecated since version 2.1.0
506
     */
507
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
508
    {
509
        $identity = $this->getIdentity();
510
511
        if (in_array($identity->getStatus(), $statuses)) {
512
            return $this->throttle($minimum, $maximum);
513
        }
514
    }
515
516
    /*
517
     * Block
518
     *
519
     * @param string $type
520
     * @param array  $extra
521
     *
522
     */
523
    public function block($type = null, $extra = null)
524
    {
525
        $this->context['blocked'] = true;
526
527
        if (isset($type)) {
528
            $this->registerEvent($type, $extra);
529
        }
530
531
        if (is_callable($this->responseCodeHandler)) {
532
            $responseCodeHandler = $this->responseCodeHandler;
533
            $responseCodeHandler(403, 'Forbidden');
534
        }
535
        else {
536
            $this->error('No response code handler available.');
537
        }
538
539
        if (is_callable($this->exitHandler)) {
540
            $exitHandler = $this->exitHandler;
541
            $exitHandler();
542
        }
543
        else {
544
            // $this->error('No exit callable set. PHP exit construct will be used.');
545
            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...
546
        }
547
    }
548
549
    /*
550
     * @deprecated deprecated since version 2.1.0
551
     */
552
    public function ban($statuses = array())
553
    {
554
        $identity = $this->getIdentity();
555
556
        if (in_array($identity->getStatus(), $statuses)) {
557
            $this->context['banned'] = true;
558
            return $this->block();
559
        }
560
561
    }
562
563
    /*
564
     * @param string $type
565
     * @param array  $extra
566
     */
567
    public function registerEvent($type, $extra = null)
568
    {
569
        $this->context['event']['type'] = $type;
570
        if (!empty($extra)) {
571
            $this->context['event']['extra'] = $extra;
572
        }
573
    }
574
575
    /*
576
     * Complete the connection then attempt to log.
577
     */
578
    public function end()
579
    {
580
        // Already ended, skip
581
        if ($this->ended === true) {
582
            return;
583
        }
584
585
        $this->completeContext();
586
        $this->completeResponse();
587
588
        // We really want to avoid throwing exceptions there
589
        try {
590
            $this->log();
591
        } catch (Exception $e) {
592
            error_log($e->getMessage());
593
        }
594
595
        $this->ended = true;
596
    }
597
598
    /*
599
     * Log the connection to the logging backend.
600
     */
601
    public function log()
602
    {
603
        $logEntry = array(
604
            'address'  => $this->getAddress(),
605
            'request'  => $this->getRequest(),
606
            'response' => $this->getResponse(),
607
            'identity' => $this->getIdentity(),
608
            'session'  => $this->getSessionId(),
609
            'context'  => $this->getContext(),
610
        );
611
612
        $logger = $this->getLogger();
613
        if ($logger) {
614
            $logger->log($logEntry);
615
        }
616
    }
617
618
    // Static
619
620
    public static function hash($value)
621
    {
622
        return md5($value);
623
    }
624
625
}
626