Completed
Push — master ( 5d22ce...12a557 )
by François
02:57
created

Bouncer::getAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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