PageRenderTime 55ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Bouncer.php

http://github.com/znarf/Bouncer
PHP | 645 lines | 343 code | 102 blank | 200 comment | 41 complexity | 657254ae94d845690851a22b630c20fc MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Bouncer package.
  4. *
  5. * (c) François Hodierne <francois@hodierne.net>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Bouncer;
  11. use Bouncer\Resource\Address;
  12. use Bouncer\Resource\Identity;
  13. class Bouncer
  14. {
  15. const NICE = 'nice';
  16. const OK = 'ok';
  17. const SUSPICIOUS = 'suspicious';
  18. const BAD = 'bad';
  19. const ROBOT = 'robot';
  20. const BROWSER = 'browser';
  21. const UNKNOWN = 'unknown';
  22. /**
  23. * @var array
  24. */
  25. public static $supportedOptions = array(
  26. 'cache',
  27. 'request',
  28. 'logger',
  29. 'profile',
  30. 'cookieName',
  31. 'cookiePath',
  32. 'exitHandler',
  33. 'responseCodeHandler'
  34. );
  35. /**
  36. * @var string|object
  37. */
  38. protected $profile;
  39. /**
  40. * @var boolean
  41. */
  42. protected $throwExceptions = false;
  43. /**
  44. * @var boolean
  45. */
  46. protected $logErrors = true;
  47. /**
  48. * @var string
  49. */
  50. protected $cookieName = 'bsid';
  51. /**
  52. * @var string
  53. */
  54. protected $cookiePath = '/';
  55. /**
  56. * The exit callable to use when blocking a request
  57. *
  58. * @var callable
  59. */
  60. protected $exitHandler;
  61. /**
  62. * The callable to use to set the HTTP Response Code
  63. *
  64. * @var callable
  65. */
  66. protected $responseCodeHandler;
  67. /**
  68. * @var \Bouncer\Cache\CacheInterface
  69. */
  70. protected $cache;
  71. /**
  72. * @var \Bouncer\Logger\LoggerInterface
  73. */
  74. protected $logger;
  75. /**
  76. * @var Request
  77. */
  78. protected $request;
  79. /**
  80. * @var array
  81. */
  82. protected $response;
  83. /**
  84. * @var array
  85. */
  86. protected $analyzers = array();
  87. /**
  88. * @var Identity
  89. */
  90. protected $identity;
  91. /**
  92. * Store internal metadata
  93. *
  94. * @var array
  95. */
  96. protected $context;
  97. /**
  98. * @var boolean
  99. */
  100. protected $started = false;
  101. /**
  102. * @var boolean
  103. */
  104. protected $ended = false;
  105. /**
  106. * Constructor.
  107. *
  108. * @param array $options
  109. */
  110. public function __construct(array $options = array())
  111. {
  112. if (!empty($options)) {
  113. $this->setOptions($options);
  114. }
  115. // Load Profile
  116. if (!$this->profile) {
  117. $this->profile = new \Bouncer\Profile\DefaultProfile;
  118. }
  119. call_user_func_array(array($this->profile, 'load'), array($this));
  120. }
  121. /*
  122. * Set the supported options
  123. */
  124. public function setOptions(array $options = array())
  125. {
  126. foreach (static::$supportedOptions as $key) {
  127. if (isset($options[$key])) {
  128. $this->$key = $options[$key];
  129. }
  130. }
  131. }
  132. /**
  133. * @throw Exception
  134. */
  135. public function error($message)
  136. {
  137. if ($this->throwExceptions) {
  138. throw new Exception($message);
  139. }
  140. if ($this->logErrors) {
  141. error_log("Bouncer: {$message}");
  142. }
  143. }
  144. /**
  145. * @return \Bouncer\Cache\CacheInterface
  146. */
  147. public function getCache($reportError = false)
  148. {
  149. if (empty($this->cache)) {
  150. if ($reportError) {
  151. $this->error('No cache available.');
  152. }
  153. return;
  154. }
  155. return $this->cache;
  156. }
  157. /**
  158. * @return \Bouncer\Logger\LoggerInterface
  159. */
  160. public function getLogger($reportError = false)
  161. {
  162. if (empty($this->logger)) {
  163. if ($reportError) {
  164. $this->error('No logger available.');
  165. }
  166. return;
  167. }
  168. return $this->logger;
  169. }
  170. /**
  171. * @return Request
  172. */
  173. public function getRequest()
  174. {
  175. if (isset($this->request)) {
  176. return $this->request;
  177. }
  178. $request = Request::createFromGlobals();
  179. $request->setTrustedProxies(array('127.0.0.1'));
  180. $request->setTrustedHeaderName(Request::HEADER_FORWARDED, null);
  181. return $this->request = $request;
  182. }
  183. /**
  184. * @return array
  185. */
  186. public function getResponse()
  187. {
  188. return $this->response;
  189. }
  190. /**
  191. * @return string
  192. */
  193. public function getUserAgent()
  194. {
  195. return $this->getRequest()->getUserAgent();
  196. }
  197. /**
  198. * @return string
  199. */
  200. public function getAddr()
  201. {
  202. return $this->getRequest()->getAddr();
  203. }
  204. /**
  205. * @return Address
  206. */
  207. public function getAddress()
  208. {
  209. $addr = $this->getRequest()->getAddr();
  210. $address = new Address($addr);
  211. return $address;
  212. }
  213. /**
  214. * @return array
  215. */
  216. public function getHeaders()
  217. {
  218. $request = $this->getRequest();
  219. $headers = $request->getHeaders();
  220. return $headers;
  221. }
  222. /**
  223. * @return Signature
  224. */
  225. public function getSignature()
  226. {
  227. $headers = $this->getHeaders();
  228. $signature = new Signature(array('headers' => $headers));
  229. return $signature;
  230. }
  231. /**
  232. * @return array
  233. */
  234. public function getCookies()
  235. {
  236. $names = array($this->cookieName, '__utmz', '__utma');
  237. $request = $this->getRequest();
  238. return $request->getCookies($names);
  239. }
  240. /**
  241. * Return the current session id (from Cookie)
  242. *
  243. * @return string|null
  244. */
  245. public function getSessionId()
  246. {
  247. $request = $this->getRequest();
  248. return $request->getCookie($this->cookieName);
  249. }
  250. /**
  251. * Return the protocol of the request: HTTP/1.0 or HTTP/1.1
  252. *
  253. * @return string|null
  254. */
  255. public function getProtocol()
  256. {
  257. $request = $this->getRequest();
  258. return $request->getProtocol();
  259. }
  260. /**
  261. * @return Identity
  262. */
  263. public function getIdentity()
  264. {
  265. if (isset($this->identity)) {
  266. return $this->identity;
  267. }
  268. $cache = $this->getCache();
  269. $identity = new Identity(array(
  270. 'address' => $this->getAddress(),
  271. 'headers' => $this->getHeaders(),
  272. 'session' => $this->getSessionId(),
  273. ));
  274. $id = $identity->getId();
  275. // Try to get Identity from cache
  276. if ($cache) {
  277. $cacheIdentity = $cache->getIdentity($id);
  278. if ($cacheIdentity instanceof Identity) {
  279. return $this->identity = $cacheIdentity;
  280. }
  281. }
  282. // Process Analyzers
  283. if (!$this->ended) {
  284. $identity = $this->processAnalyzers('identity', $identity);
  285. // Store Identity in cache
  286. if ($cache) {
  287. $cache->setIdentity($id, $identity);
  288. }
  289. else {
  290. $this->error('No cache available. Caching identity is needed to keep performances acceptable.');
  291. }
  292. }
  293. return $this->identity = $identity;
  294. }
  295. /**
  296. * @return array
  297. */
  298. public function getContext()
  299. {
  300. if (!isset($this->context)) {
  301. $this->initContext();
  302. }
  303. return $this->context;
  304. }
  305. /*
  306. * Init the context with time and pid.
  307. */
  308. public function initContext()
  309. {
  310. $this->context = array();
  311. $this->addContext('time', microtime(true));
  312. $this->addContext('bouncer', array('pid' => getmypid()));
  313. }
  314. /*
  315. * @param string $key
  316. * @param boolean|string|array $properties
  317. */
  318. public function addContext($key, $properties)
  319. {
  320. if (isset($this->context[$key]) && is_array($this->context[$key])) {
  321. $this->context[$key] = array_merge($this->context[$key], $properties);
  322. } else {
  323. $this->context[$key] = $properties;
  324. }
  325. }
  326. /*
  327. * Complete the context with session, exec_time and memory_usage.
  328. */
  329. public function completeContext()
  330. {
  331. $context = $this->getContext();
  332. // Session Id (from Cookie)
  333. $sessionId = $this->getSessionId();
  334. if (isset($sessionId)) {
  335. $this->addContext('session', $sessionId);
  336. }
  337. // Measure execution time
  338. $execution_time = microtime(true) - $context['time'];
  339. if (!empty($context['bouncer']['throttle_time'])) {
  340. $execution_time -= $context['bouncer']['throttle_time'];
  341. }
  342. $this->addContext('bouncer', array(
  343. 'execution_time' => round($execution_time, 4),
  344. 'memory_usage' => memory_get_peak_usage(),
  345. ));
  346. }
  347. /*
  348. * Complete the response with status code
  349. */
  350. public function completeResponse()
  351. {
  352. if (!isset($this->response)) {
  353. $this->response = array();
  354. }
  355. if (is_callable($this->responseCodeHandler)) {
  356. $responseCodeHandler = $this->responseCodeHandler;
  357. $responseStatus = $responseCodeHandler();
  358. if ($responseStatus) {
  359. $this->response['status'] = $responseStatus;
  360. }
  361. }
  362. }
  363. /*
  364. * Register an analyzer for a given type.
  365. *
  366. * @param string
  367. * @param callable
  368. * @param int
  369. */
  370. public function registerAnalyzer($type, $callable, $priority = 100)
  371. {
  372. $this->analyzers[$type][] = array($callable, $priority);
  373. }
  374. /*
  375. * Process Analyzers for a given type. Return the modified array or object.
  376. *
  377. * @param string
  378. * @param object
  379. *
  380. * @return object
  381. */
  382. protected function processAnalyzers($type, $value)
  383. {
  384. if (isset($this->analyzers[$type])) {
  385. // TODO: order analyzers by priority
  386. foreach ($this->analyzers[$type] as $array) {
  387. list($callable) = $array;
  388. $value = call_user_func_array($callable, array($value));
  389. }
  390. }
  391. return $value;
  392. }
  393. /*
  394. * Start Bouncer, init context and register end function
  395. */
  396. public function start()
  397. {
  398. // Already started, skip
  399. if ($this->started === true) {
  400. return;
  401. }
  402. $this->initContext();
  403. register_shutdown_function(array($this, 'end'));
  404. $this->started = true;
  405. }
  406. /*
  407. * Set a cookie containing the session id
  408. */
  409. public function initSession()
  410. {
  411. $identity = $this->getIdentity();
  412. $identitySession = $identity->getSession();
  413. if ($identitySession) {
  414. $curentSessionId = $this->getSessionId();
  415. $identitySessionId = $identitySession->getId();
  416. if (empty($curentSessionId) || $curentSessionId !== $identitySessionId) {
  417. setcookie($this->cookieName, $identitySessionId, time() + (60 * 60 * 24 * 365 * 2), $this->cookiePath);
  418. }
  419. }
  420. }
  421. /*
  422. * Throttle
  423. *
  424. * @param int $minimum in milliseconds
  425. * @param int $maximum in milliseconds
  426. *
  427. */
  428. public function throttle($minimum = 1000, $maximum = 2500)
  429. {
  430. // In microseconds
  431. $throttleTime = rand($minimum * 1000, $maximum * 1000);
  432. usleep($throttleTime);
  433. // In seconds
  434. $this->addContext('bouncer', array(
  435. 'throttle_time' => ($throttleTime / 1000 / 1000)
  436. ));
  437. }
  438. /*
  439. * @deprecated deprecated since version 2.1.0
  440. */
  441. public function sleep($statuses = array(), $minimum = 1000, $maximum = 2500)
  442. {
  443. $identity = $this->getIdentity();
  444. if (in_array($identity->getStatus(), $statuses)) {
  445. return $this->throttle($minimum, $maximum);
  446. }
  447. }
  448. /*
  449. * Block
  450. *
  451. * @param string $type
  452. * @param array $extra
  453. *
  454. */
  455. public function block($type = null, $extra = null)
  456. {
  457. $this->addContext('blocked', true);
  458. if (isset($type)) {
  459. $this->registerEvent($type, $extra);
  460. }
  461. if (is_callable($this->responseCodeHandler)) {
  462. $responseCodeHandler = $this->responseCodeHandler;
  463. $responseCodeHandler(403, 'Forbidden');
  464. }
  465. else {
  466. $this->error('No response code handler available.');
  467. }
  468. if (is_callable($this->exitHandler)) {
  469. $exitHandler = $this->exitHandler;
  470. $exitHandler();
  471. }
  472. else {
  473. // $this->error('No exit callable set. PHP exit construct will be used.');
  474. exit;
  475. }
  476. }
  477. /*
  478. * @deprecated deprecated since version 2.1.0
  479. */
  480. public function ban($statuses = array())
  481. {
  482. $identity = $this->getIdentity();
  483. if (in_array($identity->getStatus(), $statuses)) {
  484. return $this->block();
  485. }
  486. }
  487. /*
  488. * @param string $type
  489. * @param array $extra
  490. */
  491. public function registerEvent($type, $extra = null)
  492. {
  493. $this->context['event']['type'] = $type;
  494. if (!empty($extra)) {
  495. $this->context['event']['extra'] = $extra;
  496. }
  497. }
  498. /*
  499. * Complete the connection then attempt to log.
  500. */
  501. public function end()
  502. {
  503. // Already ended, skip
  504. if ($this->ended === true) {
  505. return;
  506. }
  507. if (function_exists('fastcgi_finish_request')) {
  508. fastcgi_finish_request();
  509. }
  510. $this->ended = true;
  511. $this->completeContext();
  512. $this->completeResponse();
  513. // We really want to avoid throwing exceptions there
  514. try {
  515. $this->log();
  516. } catch (Exception $e) {
  517. error_log($e->getMessage());
  518. }
  519. }
  520. /*
  521. * Log the connection to the logging backend.
  522. */
  523. public function log()
  524. {
  525. $logEntry = array(
  526. 'address' => $this->getAddress(),
  527. 'request' => $this->getRequest(),
  528. 'response' => $this->getResponse(),
  529. 'identity' => $this->getIdentity(),
  530. 'session' => $this->getSessionId(),
  531. 'context' => $this->getContext(),
  532. );
  533. $logger = $this->getLogger();
  534. if ($logger) {
  535. $logger->log($logEntry);
  536. }
  537. }
  538. // Static
  539. public static function hash($value)
  540. {
  541. return md5($value);
  542. }
  543. }