From e7ed2473937fe2d52f5dea8d7d07932175e90bd8 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Tue, 1 May 2012 14:49:54 -0400 Subject: [PATCH] [FlashPolicy] Updating the flash policy component --- .../Component/Server/FlashPolicyComponent.php | 236 ++++++++++++++++++ .../Server/FlashPolicyComponentTest.php | 166 ++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 src/Ratchet/Component/Server/FlashPolicyComponent.php create mode 100644 tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php diff --git a/src/Ratchet/Component/Server/FlashPolicyComponent.php b/src/Ratchet/Component/Server/FlashPolicyComponent.php new file mode 100644 index 0000000..56a0d1b --- /dev/null +++ b/src/Ratchet/Component/Server/FlashPolicyComponent.php @@ -0,0 +1,236 @@ +'; + + + protected $_access = array(); + protected $_headers = array(); + protected $_siteControl = ''; + + protected $_cache = ''; + + protected $_cacheValid = false; + + /** + * @{inheritdoc} + */ + public function onOpen(ConnectionInterface $conn) { + $conn->PolicyRequest = ''; + } + + /** + * @{inheritdoc} + */ + public function onMessage(ConnectionInterface $from, $msg) { + + if (!$this->_cacheValid) { + $this->_cache = $this->renderPolicy()->asXML(); + $this->_cacheValid = true; + } + + $from->PolicyRequest .= $msg; + if (strlen($from->_cache) < 20) { + return; + } + + + $cmd = new SendMessage($from); + $cmd->setMessage($this->_cache . "\0"); + + return $cmd; + } + + /** + * @{inheritdoc} + */ + public function onClose(ConnectionInterface $conn) { + } + + /** + * @{inheritdoc} + */ + public function onError(ConnectionInterface $conn, \Exception $e) { + return new CloseConnection($conn); + } + + + public function setSiteControl($permittedCrossDomainPolicies = 'all') { + if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { + throw new \UnexpectedValueException('Invalid site control set'); + } + $this->_siteControl = $permittedCrossDomainPolicies; + } + + public function renderPolicy() { + + $policy = new \SimpleXMLElement($this->_policy); + + + $siteControl = $policy->addChild('site-control'); + + if ($this->_siteControl == '') { + throw new \UnexpectedValueException('Where\'s my site control?'); + } + $siteControl->addAttribute('permitted-cross-domain-policies', $this->_siteControl); + + + if (empty($this->_access)) { + throw new \UnexpectedValueException('Missing site access'); + } + foreach ($this->_access as $access) { + + $tmp = $policy->addChild('allow-access-from'); + $tmp->addAttribute('domain', $access[0]); + $tmp->addAttribute('to-ports', $access[1]); + $tmp->addAttribute('secure', ($access[2] == true) ? 'true' : 'false'); + } + + foreach ($this->_headers as $header) { + + $tmp = $policy->addChild('allow-http-request-headers-from'); + $tmp->addAttribute('domain', $access[0]); + $tmp->addAttribute('headers', $access[1]); + $tmp->addAttribute('secure', ($access[2] == true) ? 'true' : 'false'); + } + + return $policy; + + } + + public function addAllowedAccess($domain, $ports = '*', $secure = false) { + + if (!$this->validateDomain($domain)) { + throw new \UnexpectedValueException('Invalid domain'); + } + if (!$this->validatePorts($ports)) { + throw new \UnexpectedValueException('Invalid Port'); + } + + + $this->_access[] = array($domain, $ports, $secure); + $this->_cacheValid = false; + } + + public function addAllowedHTTPRequestHeaders($domain, $headers, $secure = true) { + + if (!$this->validateDomain($domain)) { + throw new \UnexpectedValueException('Invalid domain'); + } + if (!$this->validateHeaders($headers)) { + throw new \UnexpectedValueException('Invalid Header'); + } + $this->_headers[] = array($domain, $headers, (string)$secure); + $this->_cacheValid = false; + } + + + public function validateSiteControl($permittedCrossDomainPolicies) { + + return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'by-content-type', 'all')); + } + + public function validateDomain($domain) { + + if ($domain == '*') { + return true; + } + + if (filter_var($domain, FILTER_VALIDATE_IP)) { + return true; + } + + + $d = parse_url($domain); + if (!isset($d['scheme']) || empty($d['scheme'])) { + $domain = 'http://' . $domain; + } + + if (substr($domain, -1) == '*') { + return false; + } + + $d = parse_url($domain); + + $parts = explode('.', $d['host']); + $tld = array_pop($parts); + + if (($pos = strpos($tld, '*')) !== false) { + return false; + } + + return (bool)filter_var(str_replace(array('*.', '.*'), '123', $domain), FILTER_VALIDATE_URL); + } + + public function validatePorts($port) { + + if ($port == '*') { + return true; + } + + $ports = explode(',', $port); + + foreach ($ports as $port) { + $range = substr_count($port, '-'); + + if ($range > 1) { + return false; + } else if ($range == 1) { + $ranges = explode('-', $port); + + if (!is_numeric($ranges[0]) || !is_numeric($ranges[1]) || $ranges[0] > $ranges[1]) { + return false; + } else { + return true; + } + } + + if (!is_numeric($port) || $port == '') { + return false; + } + } + + return true; + } + + public function validateHeaders($headers) { + + if ($headers == '*') { + return true; + } + $headers = explode(',', $headers); + + foreach ($headers as $header) { + + if ((bool)preg_match('/.*\*+.+/is', $header)) { + return false; + } + + if(!ctype_alnum(str_replace(array('-', '_', '*' ), '', $header))) { + return false; + } + } + + return true; + } + + public function validateSecure($secure) { + + return is_bool($secure); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php new file mode 100644 index 0000000..16f6659 --- /dev/null +++ b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php @@ -0,0 +1,166 @@ +_policy = new FlashPolicyComponent(); + } + + + public function testPolicyRender() { + $this->_policy->setSiteControl('all'); + $this->_policy->addAllowedAccess('example.com', '*'); + $this->_policy->addAllowedAccess('dev.example.com', '*'); + $this->_policy->addAllowedHTTPRequestHeaders('*', '*'); + $this->assertInstanceOf('SimpleXMLElement', $this->_policy->renderPolicy()); + } + + public function testInvalidPolicyReader() { + $this->setExpectedException('UnexpectedValueException'); + $this->_policy->addAllowedHTTPRequestHeaders('*', '*'); + $this->_policy->renderPolicy(); + } + + public function testAnotherInvalidPolicyReader() { + $this->setExpectedException('UnexpectedValueException'); + $this->_policy->addAllowedHTTPRequestHeaders('*', '*'); + $this->_policy->addAllowedAccess('dev.example.com', '*'); + $this->_policy->renderPolicy(); + } + + public function testInvalidDomainPolicyReader() { + $this->setExpectedException('UnexpectedValueException'); + $this->_policy->setSiteControl('all'); + $this->_policy->addAllowedHTTPRequestHeaders('*', '*'); + $this->_policy->addAllowedAccess('dev.example.*', '*'); + $this->_policy->renderPolicy(); + } + + + /** + * @dataProvider siteControl + */ + public function testSiteControlValidation($accept, $permittedCrossDomainPolicies) { + $this->assertEquals($accept, $this->_policy->validateSiteControl($permittedCrossDomainPolicies)); + } + + public static function siteControl() { + return array( + array(true, 'all') + , array(true, 'none') + , array(true, 'master-only') + , array(true, 'by-content-type') + , array(false, 'by-ftp-filename') + , array(false, '') + , array(false, 'all ') + , array(false, 'asdf') + , array(false, '@893830') + , array(false, '*') + ); + } + + + /** + * @dataProvider URI + */ + public function testDomainValidation($accept, $domain) { + $this->assertEquals($accept, $this->_policy->validateDomain($domain)); + } + + public static function URI() { + return array( + array(true, '*') + , array(true, 'example.com') + , array(true, 'exam-ple.com') + , array(true, 'www.example.com') + , array(true, 'http://example.com') + , array(true, 'http://*.example.com') + , array(false, 'exam*ple.com') + , array(true, '127.0.0.1') + , array(true, 'localhost') + , array(false, 'www.example.*') + , array(false, 'www.exa*le.com') + , array(false, 'www.example.*com') + , array(false, '*.example.*') + , array(false, 'gasldf*$#a0sdf0a8sdf') + , array(false, 'http://example.*') + ); + } + + + /** + * @dataProvider ports + */ + public function testPortValidation($accept, $ports) { + $this->assertEquals($accept, $this->_policy->validatePorts($ports)); + } + + public static function ports() { + return array( + array(true, '*') + , array(true, '80') + , array(true, '80,443') + , array(true, '507,516-523') + , array(false, '233-11') + , array(true, '507,516-523,333') + , array(true, '507,516-523,507,516-523') + , array(true, '516-523') + , array(true, '516-523,11') + , array(false, 'example') + , array(false, 'asdf,123') + , array(false, '--') + , array(false, ',,,') + , array(false, '838*') + ); + } + + /** + * @dataProvider headers + */ + public function testHeaderValidation($accept, $headers) { + $this->assertEquals($accept, $this->_policy->validateHeaders($headers)); + } + + public static function headers() { + return array( + array(true, '*') + , array(true, 'X-Foo') + , array(true, 'X-Foo*,hello') + , array(false, 'X-Fo*o,hello') + , array(false, '*ooo,hello') + , array(false, 'X Foo') + , array(false, false) + , array(true, 'X-001') + , array(false, '--') + , array(false, '-') + ); + } + + /** + * @dataProvider bools + */ + public function testSecureValidation($accept, $bool) { + $this->assertEquals($accept, $this->_policy->validateSecure($bool)); + } + + public static function bools() { + return array( + array(true, true) + , array(true, false) + , array(false, 1) + , array(false, 0) + , array(false, 'false') + , array(false, 'on') + , array(false, 'yes') + , array(false, '--') + , array(false, '!') + ); + } +} \ No newline at end of file