Completed
Push — master ( c5d4b5...b95cec )
by François
02:06
created

Bouncer::hash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
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 string|object
31
     */
32
    protected $profile;
33
34
    /**
35
     * @var boolean
36
     */
37
    protected $throwExceptions = false;
38
39
    /**
40
     * @var boolean
41
     */
42
    protected $logErrors = true;
43
44
    /**
45
     * @var string
46
     */
47
    protected $cookieName = 'bsid';
48
49
    /**
50
     * @var string
51
     */
52
    protected $cookiePath = '/';
53
54
    /**
55
     * @var \Bouncer\Cache\CacheInterface
56
     */
57
    protected $cache;
58
59
    /**
60
     * @var \Bouncer\Logger\LoggerInterface
61
     */
62
    protected $logger;
63
64
    /**
65
     * @var Request
66
     */
67
    protected $request;
68
69
    /**
70
     * @var array
71
     */
72
    protected $response;
73
74
    /**
75
     * @var array
76
     */
77
    protected $analyzers = array();
78
79
    /**
80
     * @var Identity
81
     */
82
    protected $identity;
83
84
    /**
85
     * Store internal metadata
86
     *
87
     * @var array
88
     */
89
    protected $context;
90
91
    /**
92
     * @var boolean
93
     */
94
    protected $started = false;
95
96
    /**
97
     * @var boolean
98
     */
99
    protected $ended = false;
100
101
    public function __construct(array $options = array())
102
    {
103
        if (!empty($options)) {
104
            $this->setOptions($options);
105
        }
106
107
        // Load Profile
108
        if (!$this->profile) {
109
            $this->profile = new \Bouncer\Profile\Standard;
110
        }
111
112
        call_user_func_array(array($this->profile, 'load'), array($this));
113
    }
114
115
    /*
116
     * Set the supported options
117
     */
118
    public function setOptions(array $options = array())
119
    {
120
        if (isset($options['cache'])) {
121
            $this->cache = $options['cache'];
122
        }
123
        if (isset($options['request'])) {
124
            $this->request = $options['request'];
125
        }
126
        if (isset($options['logger'])) {
127
            $this->logger = $options['logger'];
128
        }
129
        if (isset($options['profile'])) {
130
            $this->profile = $options['profile'];
131
        }
132
        if (isset($options['cookieName'])) {
133
            $this->cookieName = $options['cookieName'];
134
        }
135
        if (isset($options['cookiePath'])) {
136
            $this->cookiePath = $options['cookiePath'];
137
        }
138
    }
139
140
    /**
141
     * @throw Exception
142
     */
143
    public function error($message)
144
    {
145
        if ($this->throwExceptions) {
146
            throw new Exception($message);
147
        }
148
        if ($this->logErrors) {
149
            error_log("Bouncer: {$message}");
150
        }
151
    }
152
153
    /**
154
     * @return \Bouncer\Cache\CacheInterface
155
     */
156
    public function getCache($reportError = false)
157
    {
158
        if (empty($this->cache)) {
159
            if ($reportError) {
160
                $this->error('No cache available.');
161
            }
162
            return;
163
        }
164
165
        return $this->cache;
166
    }
167
168
    /**
169
     * @return \Bouncer\Logger\LoggerInterface
170
     */
171
    public function getLogger($reportError = false)
172
    {
173
        if (empty($this->logger)) {
174
            if ($reportError) {
175
                $this->error('No logger available.');
176
            }
177
            return;
178
        }
179
180
        return $this->logger;
181
    }
182
183
    /**
184
     * @return Request
185
     */
186
    public function getRequest()
187
    {
188
        if (isset($this->request)) {
189
            return $this->request;
190
        }
191
192
        $request = Request::createFromGlobals();
193
194
        return $this->request = $request;
195
    }
196
197
    /**
198
     * @return array
199
     */
200
    public function getResponse()
201
    {
202
        return $this->response;
203
    }
204
205
    /**
206
     * @return string
207
     */
208
    public function getUserAgent()
209
    {
210
        return $this->getRequest()->getUserAgent();
211
    }
212
213
    /**
214
     * @return string
215
     */
216
    public function getAddr()
217
    {
218
        return $this->getRequest()->getAddr();
219
    }
220
221
    /**
222
     * @return Address
223
     */
224
    public function getAddress()
225
    {
226
        $addr = $this->getRequest()->getAddr();
227
228
        $address = new Address($addr);
229
230
        return $address;
231
    }
232
233
    /**
234
     * @return array
235
     */
236
    public function getHeaders()
237
    {
238
        $request = $this->getRequest();
239
240
        $headers = $request->getHeaders();
241
242
        return $headers;
243
    }
244
245
    /**
246
     * @return Signature
247
     */
248
    public function getSignature()
249
    {
250
        $headers = $this->getHeaders();
251
252
        $signature = new Signature(array('headers' => $headers));
253
254
        return $signature;
255
    }
256
257
    /**
258
     * @return array
259
     */
260
    public function getCookies()
261
    {
262
        $names = array($this->cookieName, '__utmz', '__utma');
263
264
        $request = $this->getRequest();
265
266
        return $request->getCookies($names);
267
    }
268
269
    /**
270
     * Return the current session id (from Cookie)
271
     *
272
     * @return string|null
273
     */
274
    public function getSessionId()
275
    {
276
        $request = $this->getRequest();
277
278
        return $request->getCookie($this->cookieName);
279
    }
280
281
    /**
282
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
283
     *
284
     * @return string|null
285
     */
286
    public function getProtocol()
287
    {
288
        $request = $this->getRequest();
289
290
        return $request->getProtocol();
291
    }
292
293
    /**
294
     * @return Identity
295
     */
296
    public function getIdentity()
297
    {
298
        if (isset($this->identity)) {
299
            return $this->identity;
300
        }
301
302
        $cache = $this->getCache();
303
304
        $identity = new Identity(array(
305
            'address' => $this->getAddress(),
306
            'headers' => $this->getHeaders(),
307
        ));
308
309
        $id = $identity->getId();
310
311
        // Try to get Identity from cache
312
        if ($cache) {
313
            $cacheIdentity = $cache->getIdentity($id);
314
            if ($cacheIdentity instanceof Identity) {
315
                return $this->identity = $cacheIdentity;
316
            }
317
        }
318
319
        // Process Analyzers
320
        $identity = $this->processAnalyzers('identity', $identity);
321
322
        // Store Identity in cache
323
        if ($cache) {
324
            $cache->setIdentity($id, $identity);
325
        }
326
327
        return $this->identity = $identity;
328
    }
329
330
    public function getContext()
331
    {
332
        if (!isset($this->context)) {
333
            $this->initContext();
334
        }
335
336
        return $this->context;
337
    }
338
339
    /*
340
     * Init the context with id, time and start.
341
     */
342
    public function initContext()
343
    {
344
        $this->context = array();
345
        $this->context['pid']   = getmypid();
346
        $this->context['time']  = time();
347
        $this->context['start'] = microtime(true);
348
    }
349
350
    /*
351
     * Complete the context with end, exec_time and memory_usage.
352
     */
353
    public function completeContext()
354
    {
355
        // Session Id (from Cookie)
356
        $sessionId = $this->getSessionId();
357
        if (isset($sessionId)) {
358
            $this->context['session'] = $sessionId;
359
        }
360
361
        // Measure execution time
362
        $this->context['end'] = microtime(true);
363
        $this->context['exec_time'] = round($this->context['end'] - $this->context['start'], 4);
364
        if (!empty($this->context['throttle_time'])) {
365
             $this->context['exec_time'] -= $this->context['throttle_time'];
366
        }
367
        unset($this->context['end'], $this->context['start']);
368
369
        // Report Memory Usage
370
        $this->context['memory_usage'] = memory_get_peak_usage();
371
    }
372
373
    /*
374
     * Complete the response with status code
375
     */
376
    public function completeResponse()
377
    {
378
        if (!isset($this->response)) {
379
            $this->response = array();
380
        }
381
382
        if (function_exists('http_response_code')) {
383
            $responseStatus = http_response_code();
384
            if ($responseStatus) {
385
                $this->response['status'] = $responseStatus;
386
            }
387
        }
388
    }
389
    /*
390
     * Register an analyzer for a given type.
391
     *
392
     * @param string
393
     * @param callable
394
     * @param int
395
     */
396
    public function registerAnalyzer($type, $callable, $priority = 100)
397
    {
398
        $this->analyzers[$type][] = array($callable, $priority);
399
    }
400
401
    /*
402
     * Process Analyzers for a given type. Return the modified array or object.
403
     *
404
     * @param string
405
     * @param object
406
     *
407
     * @return object
408
     */
409
    protected function processAnalyzers($type, $value)
410
    {
411
        if (isset($this->analyzers[$type])) {
412
            // TODO: order analyzers by priority
413
            foreach ($this->analyzers[$type] as $array) {
414
                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...
415
                $value = call_user_func_array($callable, array($value));
416
            }
417
        }
418
        return $value;
419
    }
420
421
    /*
422
     * Start a Bouncer session
423
     */
424
    public function start()
425
    {
426
        // Already started, skip
427
        if ($this->started === true) {
428
            return;
429
        }
430
431
        $this->initContext();
432
433
        $this->initSession();
434
435
        register_shutdown_function(array($this, 'end'));
436
437
        $this->started = true;
438
    }
439
440
    /*
441
     * Set a cookie containing the session id
442
     */
443
    public function initSession()
444
    {
445
        $identity = $this->getIdentity();
446
447
        $identitySession = $identity->getSession();
448
        if ($identitySession) {
449
            $curentSessionId = $this->getSessionId();
450
            $identitySessionId = $identitySession->getId();
451
            if (empty($curentSessionId) || $curentSessionId !== $identitySessionId) {
452
                setcookie($this->cookieName, $identitySessionId, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
453
            }
454
        }
455
    }
456
457
    /*
458
     * Sleep if Identity status is of a certain value.
459
     *
460
     * @param array $statuses
461
     * @param int   $minimum
462
     * @param int   $maximum
463
     *
464
     */
465
    public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
466
    {
467
        $identity = $this->getIdentity();
468
469
        if (in_array($identity->getStatus(), $statuses)) {
470
            $throttle_time = rand($minimum * 1000, $maximum * 1000);
471
            usleep($throttle_time);
472
            $this->context['throttle_time'] = round($throttle_time / 1000 / 1000, 3);
473
        }
474
    }
475
476
    /*
477
     * Ban if Identity status is of a certain value.
478
     */
479
    public function ban($statuses = array())
480
    {
481
        $identity = $this->getIdentity();
482
483
        if (in_array($identity->getStatus(), $statuses)) {
484
            $this->context['banned'] = true;
485
            $this->forbidden();
486
        }
487
    }
488
489
    /*
490
     * Complete the connection then attempt to log.
491
     */
492
    public function end()
493
    {
494
        // Already ended, skip
495
        if ($this->ended === true) {
496
            return;
497
        }
498
499
        $this->completeContext();
500
        $this->completeResponse();
501
502
        // We really want to avoid throwing exceptions there
503
        try {
504
            $this->log();
505
        } catch (Exception $e) {
506
            error_log($e->getMessage());
507
        }
508
509
        $this->ended = true;
510
    }
511
512
    /*
513
     * Log the connection to the logging backend.
514
     */
515
    public function log()
516
    {
517
        $logEntry = array(
518
            'address'  => $this->getAddress(),
519
            'request'  => $this->getRequest(),
520
            'response' => $this->getResponse(),
521
            'identity' => $this->getIdentity(),
522
            'context'  => $this->getContext(),
523
        );
524
525
        $logger = $this->getLogger();
526
        if ($logger) {
527
            $logger->log($logEntry);
528
        }
529
    }
530
531
    // Static
532
533
    public static function forbidden()
534
    {
535
        $code = '403';
536
        $message = 'Forbidden';
537
        self::responseStatus($code, $message);
538
        echo $message;
539
        exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method forbidden() 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...
540
    }
541
542
    public static function unavailable()
543
    {
544
        $code = '503';
545
        $message = 'Service Unavailable';
546
        self::responseStatus($code, $message);
547
        echo $message;
548
        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...
549
    }
550
551
    public static function responseStatus($code, $message)
552
    {
553
        if (function_exists('http_response_code')) {
554
            http_response_code($code);
555
        } else {
556
            header("HTTP/1.0 $code $message");
557
            header("Status: $code $message");
558
        }
559
    }
560
561
    public static function hash($value)
562
    {
563
        return md5($value);
564
    }
565
566
}
567