Completed
Push — next ( 5abbc2...607f27 )
by François
02:49
created

Bouncer::completeConnection()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 13
nc 8
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
class Bouncer
15
{
16
17
    const NICE       = 'nice';
18
    const OK         = 'ok';
19
    const SUSPICIOUS = 'suspicious';
20
    const BAD        = 'bad';
21
22
    const ROBOT      = 'robot';
23
    const BROWSER    = 'browser';
24
    const UNKNOWN    = 'unknown';
25
26
    /**
27
     * @var string|object
28
     */
29
    protected $profile = '\Bouncer\Profile\Standard';
30
31
    /**
32
     * @var boolean
33
     */
34
    protected $throwExceptions = false;
35
36
    /**
37
     * @var boolean
38
     */
39
    protected $logErrors = true;
40
41
    /**
42
     * @var boolean
43
     */
44
    protected $cookieName = 'bsid';
45
46
    /**
47
     * @var \Bouncer\Cache\CacheInterface
48
     */
49
    protected $cache;
50
51
    /**
52
     * @var \Bouncer\Logger\LoggerInterface
53
     */
54
    protected $logger;
55
56
    /**
57
     * @var Request
58
     */
59
    protected $request;
60
61
    /**
62
     * @var array
63
     */
64
    protected $analyzers = array();
65
66
    /**
67
     * @var Identity
68
     */
69
    protected $identity;
70
71
    /**
72
     * Store metadata about the handling of the request
73
     *
74
     * @var array
75
     */
76
    protected $connection = array();
77
78
    /**
79
     * @var boolean
80
     */
81
    protected $started = false;
82
83
    /**
84
     * @var boolean
85
     */
86
    protected $ended = false;
87
88
    public function __construct(array $options = array())
89
    {
90
        if (!empty($options)) {
91
            $this->setOptions($options);
92
        }
93
94
        // Load Profile
95
        $profile = $this->getProfile();
96
        if ($profile) {
97
            call_user_func_array(array($profile, 'load'), array($this));
98
        }
99
    }
100
101
    /*
102
     * Set the supported options
103
     */
104
    public function setOptions(array $options = array())
105
    {
106
        if (isset($options['cache'])) {
107
            $this->cache = $options['cache'];
108
        }
109
        if (isset($options['request'])) {
110
            $this->request = $options['request'];
111
        }
112
        if (isset($options['logger'])) {
113
            $this->logger = $options['logger'];
114
        }
115
        if (isset($options['profile'])) {
116
            $this->profile = $options['profile'];
117
        }
118
    }
119
120
    /**
121
     * @throw Exception
122
     */
123
    public function error($message)
124
    {
125
        if ($this->throwExceptions) {
126
            throw new Exception($message);
127
        }
128
        if ($this->logErrors) {
129
            error_log("Bouncer: {$message}");
130
        }
131
    }
132
133
    /**
134
     * @return \Bouncer\Cache\CacheInterface
135
     */
136
    public function getCache($reportError = false)
137
    {
138
        if (empty($this->cache)) {
139
            if ($reportError) {
140
                $this->error('No cache available.');
141
            }
142
            return;
143
        }
144
145
        return $this->cache;
146
    }
147
148
    /**
149
     * @return \Bouncer\Logger\LoggerInterface
150
     */
151
    public function getLogger($reportError = false)
152
    {
153
        if (empty($this->logger)) {
154
            if ($reportError) {
155
                $this->error('No logger available.');
156
            }
157
            return;
158
        }
159
160
        return $this->logger;
161
    }
162
163
    /**
164
     * @return string|object
165
     */
166
    public function getProfile()
167
    {
168
        return $this->profile;
169
    }
170
171
    /**
172
     * @return Request
173
     */
174
    public function getRequest()
175
    {
176
        if (isset($this->request)) {
177
            return $this->request;
178
        }
179
180
        $request = Request::createFromGlobals();
181
182
        return $this->request = $request;
183
    }
184
185
    /**
186
     * @return string
187
     */
188
    public function getUserAgent()
189
    {
190
        return $this->getRequest()->getUserAgent();
191
    }
192
193
    /**
194
     * @return string
195
     */
196
    public function getAddr()
197
    {
198
        return $this->getRequest()->getAddr();
199
    }
200
201
    /**
202
     * @return array
203
     */
204
    public function getHeaders()
205
    {
206
        $request = $this->getRequest();
207
208
        $headers = $request->getHeaders();
209
210
        // TODO: this should be deprecated
211
        $protocol = $this->getProtocol();
212
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
213
            $headers['protocol'] = $protocol;
214
        }
215
216
        return $headers;
217
    }
218
219
    /**
220
     * @return array
221
     */
222
    public function getCookies()
223
    {
224
        $names = [$this->cookieName, '__utmz', '__utma'];
225
226
        $request = $this->getRequest();
227
228
        return $request->getCookies($names);
229
    }
230
231
    /**
232
     * Return the current session id
233
     *
234
     * @return string|null
235
     */
236
    public function getSession()
237
    {
238
        $request = $this->getRequest();
239
240
        return $request->getCookie($this->cookieName);
241
    }
242
243
    /**
244
     * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
245
     *
246
     * @return string|null
247
     */
248
    public function getProtocol()
249
    {
250
        $request = $this->getRequest();
251
252
        return $request->getProtocol();
253
    }
254
255
    /**
256
     * @return Identity
257
     */
258
    public function getIdentity()
259
    {
260
        if (isset($this->identity)) {
261
            return $this->identity;
262
        }
263
264
        $cache = $this->getCache();
265
266
        $addr  = $this->getAddr();
267
        $haddr = self::hash($addr);
268
269
        $headers = $this->getHeaders();
270
        $fingerprint = Fingerprint::generate($headers);
271
272
        $id = self::hash($fingerprint . $haddr);
273
274
        // Try to get identity from cache
275
        if ($cache) {
276
            $identity = $cache->getIdentity($id);
277
            if ($identity) {
278
                return $this->identity = new Identity($identity);
279
            }
280
        }
281
282
        // Build base identity
283
        $identity = array(
284
            'id'          => $id,
285
            'addr'        => $addr,
286
            'haddr'       => $haddr,
287
            'headers'     => $headers,
288
            'fingerprint' => $fingerprint
289
        );
290
291
        // Extra identity (optional)
292
        $protocol = $this->getProtocol();
293
        if ($protocol) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $protocol 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...
294
            $identity['protocol'] = $protocol;
295
        }
296
        $cookies = $this->getCookies();
297
        if ($cookies) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cookies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
298
            $identity['cookies'] = $cookies;
299
        }
300
        $session = $this->getSession();
301
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session 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...
302
            $identity['session'] = $session;
303
        }
304
305
        // Process Analyzers
306
        $identity = $this->processAnalyzers('identity', $identity);
307
308
        // Store Identity in cache
309
        if ($cache) {
310
            $cache->setIdentity($id, $identity);
311
        }
312
313
        return $this->identity = new Identity($identity);
0 ignored issues
show
Bug introduced by
It seems like $identity defined by $this->processAnalyzers('identity', $identity) on line 306 can also be of type object; however, Bouncer\Identity::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
314
    }
315
316
    public function getConnection()
317
    {
318
        if (!$this->connection) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->connection of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
319
            $this->initConnection();
320
        }
321
322
        return $this->connection;
323
    }
324
325
    /*
326
     * Init the connection with id, time and start.
327
     */
328
    public function initConnection()
329
    {
330
        $this->connection = array();
331
        $this->connection['pid']   = getmypid();
332
        $this->connection['time']  = time();
333
        $this->connection['start'] = microtime(true);
334
    }
335
336
    /*
337
     * Complete the connection with end, exec_time, memory_usage and response_status.
338
     */
339
    public function completeConnection()
340
    {
341
        // Session (from Cookie)
342
        $session = $this->getSession();
343
        if ($session) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session 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...
344
            $this->connection['session'] = $session;
345
        }
346
347
        // Measure execution time
348
        $this->connection['end'] = microtime(true);
349
        $this->connection['exec_time'] = round($this->connection['end'] - $this->connection['start'], 4);
350
        if (!empty($this->connection['throttle_time'])) {
351
             $this->connection['exec_time'] -= $this->connection['throttle_time'];
352
        }
353
        unset($this->connection['end'], $this->connection['start']);
354
355
        // Report Memory Usage
356
        $this->connection['memory_usage'] = memory_get_peak_usage();
357
358
        // Add response
359
        $responseStatus = http_response_code();
360
        if ($responseStatus) {
361
            $this->connection['response']['status'] = $responseStatus;
362
        }
363
    }
364
365
    /*
366
     * Register an analyzer for a given type.
367
     *
368
     * @param string
369
     * @param callable
370
     * @param int
371
     */
372
    public function registerAnalyzer($type, callable $callable, $priority = 100)
373
    {
374
        $this->analyzers[$type][] = array($callable, $priority);
375
    }
376
377
    /*
378
     * Process Analyzers for a given type. Return the modified array or object.
379
     *
380
     * @param string
381
     * @param array|object
382
     *
383
     * @return array|object
384
     */
385
    protected function processAnalyzers($type, $value)
386
    {
387
        if (isset($this->analyzers[$type])) {
388
            // TODO: order analyzers by priority
389
            foreach ($this->analyzers[$type] as $array) {
390
                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...
391
                $value = call_user_func_array($callable, array($value));
392
            }
393
        }
394
        return $value;
395
    }
396
397
    /*
398
     * Start a Bouncer session
399
     */
400
    public function start()
401
    {
402
        // Already started, skip
403
        if ($this->started === true) {
404
            return;
405
        }
406
407
        $this->initConnection();
408
409
        $this->initSession();
410
411
        register_shutdown_function([$this, 'end']);
412
413
        $this->started = true;
414
    }
415
416
    /*
417
     * Set a cookie containing the session id
418
     */
419
    public function initSession()
420
    {
421
        $identity = $this->getIdentity();
422
423
        if ($identity->hasAttribute('session')) {
424
            $curentSession = $this->getSession();
425
            $identitySession = $identity->getAttribute('session');
426
            if (empty($curentSession) || $curentSession !== $identitySession) {
427
                setcookie($this->cookieName, $identitySession, time() + (60 * 60 * 365 * 2), '/');
428
            }
429
        }
430
    }
431
432
    /*
433
     * Throttle if Identity status is suspicious.
434
     */
435
    public function throttle()
436
    {
437
        $identity = $this->getIdentity();
438
439
        $status = $identity->getStatus();
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
440
441
        switch ($status) {
442
            case self::BAD:
443
                // sleep 1 to 5 seconds then exit
444
                $throttle = rand(1000*1000, 5000*1000);
445
                usleep($throttle);
446
                $this->connection['throttle_time'] = round($throttle / 1000000, 3);
447
                break;
448
            case self::SUSPICIOUS:
449
                // sleep 0.5 to 2.5 seconds then continue
450
                $throttle = rand(500*1000, 2500*1000);
451
                usleep($throttle);
452
                $this->connection['throttle_time'] = round($throttle / 1000000, 3);
453
                break;
454
        }
455
    }
456
457
    /*
458
     * Ban if Identity status is bad.
459
     */
460
    public function ban()
461
    {
462
        $identity = $this->getIdentity();
463
464
        if ($identity->getStatus() == self::BAD) {
0 ignored issues
show
Documentation Bug introduced by
The method getStatus does not exist on object<Bouncer\Identity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
465
            $this->unavailable();
466
        }
467
    }
468
469
    /*
470
     * Complete the connection then attempt to log.
471
     */
472
    public function end()
473
    {
474
        // Already ended, skip
475
        if ($this->ended === true) {
476
            return;
477
        }
478
479
        $this->completeConnection();
480
481
        // We really want to avoid throwing exceptions there
482
        try {
483
            $this->log();
484
        } catch (Exception $e) {
485
            error_log($e->getMessage());
486
        }
487
488
        $this->ended = true;
489
    }
490
491
    /*
492
     * Log the connection to the logging backend.
493
     */
494
    public function log()
495
    {
496
        $connection = $this->getConnection();
497
        $identity = $this->getIdentity();
498
        $request = $this->getRequest();
499
500
        $logger = $this->getLogger();
501
        if ($logger) {
502
            $logger->log($connection, $identity, $request);
503
        }
504
    }
505
506
    // Static
507
508
    public static function unavailable()
509
    {
510
        $code = '503';
511
        $msg = 'Service Unavailable';
512
        header("HTTP/1.0 $code $msg");
513
        header("Status: $code $msg");
514
        echo $msg;
515
        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...
516
    }
517
518
    public static function hash($string)
519
    {
520
        return md5($string);
521
    }
522
523
}
524