From e7ed2473937fe2d52f5dea8d7d07932175e90bd8 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Tue, 1 May 2012 14:49:54 -0400 Subject: [PATCH 1/8] [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 From 26d339dec7f0afa37d661a5303e50717d0e8dfba Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Tue, 1 May 2012 14:50:29 -0400 Subject: [PATCH 2/8] Hixie now returns a Guzzle object for the handshake --- src/Ratchet/Component/WebSocket/Version/Hixie76.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ratchet/Component/WebSocket/Version/Hixie76.php b/src/Ratchet/Component/WebSocket/Version/Hixie76.php index 1bbbd03..33aab7b 100644 --- a/src/Ratchet/Component/WebSocket/Version/Hixie76.php +++ b/src/Ratchet/Component/WebSocket/Version/Hixie76.php @@ -24,8 +24,8 @@ class Hixie76 implements VersionInterface { } /** - * @param string - * @return string + * @param Guzzle\Http\Message\RequestInterface + * @return Guzzle\Http\Message\Response */ public function handshake(RequestInterface $request) { $body = $this->sign($request->getHeader('Sec-WebSocket-Key1'), $request->getHeader('Sec-WebSocket-Key2'), $request->getBody()); From d0e471f4e0957a8748b233cfbe633cd059349549 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Tue, 1 May 2012 14:50:45 -0400 Subject: [PATCH 3/8] Updating the lock file --- composer.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 496e1fb..11c0a03 100644 --- a/composer.lock +++ b/composer.lock @@ -6,6 +6,10 @@ "version": "2.2.x-dev", "source-reference": "1e0aa60d109c630d19543d999f12e2852ef8f932" }, + { + "package": "guzzle", + "version": "2.0.2" + }, { "package": "guzzle/guzzle", "version": "v2.0.2" @@ -24,7 +28,7 @@ { "package": "symfony/validator", "version": "dev-master", - "source-reference": "704f655d060b14475d7bd2a0b6d653c70f88218a" + "source-reference": "dac248b43b62d30023dd9b73ad7e5b7bc1128e5e" } ], "packages-dev": null, From 532323c497f42ebe5a6d1e1baf5f29da9d68be76 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Thu, 3 May 2012 06:22:23 -0400 Subject: [PATCH 4/8] Code cleanup and start of some commenting --- .../Component/Server/FlashPolicyComponent.php | 117 +++++++++++++----- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/src/Ratchet/Component/Server/FlashPolicyComponent.php b/src/Ratchet/Component/Server/FlashPolicyComponent.php index 56a0d1b..aea3629 100644 --- a/src/Ratchet/Component/Server/FlashPolicyComponent.php +++ b/src/Ratchet/Component/Server/FlashPolicyComponent.php @@ -16,15 +16,12 @@ use Ratchet\Resource\Command\CommandInterface; */ class FlashPolicyComponent implements MessageComponentInterface { - protected $_policy = ''; - - - protected $_access = array(); - protected $_headers = array(); + protected $_policy = ''; + protected $_access = array(); + protected $_headers = array(); protected $_siteControl = ''; - protected $_cache = ''; - + protected $_cache = ''; protected $_cacheValid = false; /** @@ -70,26 +67,39 @@ class FlashPolicyComponent implements MessageComponentInterface { } + /** + * setSiteControl function. + * + * @access public + * @param string $permittedCrossDomainPolicies (default: 'all') + * @return void + */ public function setSiteControl($permittedCrossDomainPolicies = 'all') { if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { throw new \UnexpectedValueException('Invalid site control set'); } $this->_siteControl = $permittedCrossDomainPolicies; } - + + /** + * renderPolicy function. + * + * @access public + * @return void + */ public function renderPolicy() { - $policy = new \SimpleXMLElement($this->_policy); + $policy = new \SimpleXMLElement($this->_policy); - $siteControl = $policy->addChild('site-control'); - + $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'); } @@ -108,27 +118,45 @@ class FlashPolicyComponent implements MessageComponentInterface { $tmp->addAttribute('headers', $access[1]); $tmp->addAttribute('secure', ($access[2] == true) ? 'true' : 'false'); } - + return $policy; - + } - + + /** + * addAllowedAccess function. + * + * @access public + * @param mixed $domain + * @param string $ports (default: '*') + * @param bool $secure (default: false) + * @return void + */ 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; } + /** + * addAllowedHTTPRequestHeaders function. + * + * @access public + * @param mixed $domain + * @param mixed $headers + * @param bool $secure (default: true) + * @return void + */ public function addAllowedHTTPRequestHeaders($domain, $headers, $secure = true) { - + if (!$this->validateDomain($domain)) { throw new \UnexpectedValueException('Invalid domain'); } @@ -138,13 +166,26 @@ class FlashPolicyComponent implements MessageComponentInterface { $this->_headers[] = array($domain, $headers, (string)$secure); $this->_cacheValid = false; } - - + + /** + * validateSiteControl function. + * + * @access public + * @param mixed $permittedCrossDomainPolicies + * @return void + */ public function validateSiteControl($permittedCrossDomainPolicies) { - - return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'by-content-type', 'all')); + + return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'by-content-type', 'all')); } - + + /** + * validateDomain function. + * + * @access public + * @param mixed $domain + * @return void + */ public function validateDomain($domain) { if ($domain == '*') { @@ -155,7 +196,6 @@ class FlashPolicyComponent implements MessageComponentInterface { return true; } - $d = parse_url($domain); if (!isset($d['scheme']) || empty($d['scheme'])) { $domain = 'http://' . $domain; @@ -176,9 +216,16 @@ class FlashPolicyComponent implements MessageComponentInterface { return (bool)filter_var(str_replace(array('*.', '.*'), '123', $domain), FILTER_VALIDATE_URL); } - + + /** + * validatePorts function. + * + * @access public + * @param mixed $port + * @return void + */ public function validatePorts($port) { - + if ($port == '*') { return true; } @@ -208,6 +255,13 @@ class FlashPolicyComponent implements MessageComponentInterface { return true; } + /** + * validateHeaders function. + * + * @access public + * @param mixed $headers + * @return void + */ public function validateHeaders($headers) { if ($headers == '*') { @@ -229,6 +283,13 @@ class FlashPolicyComponent implements MessageComponentInterface { return true; } + /** + * validateSecure function. + * + * @access public + * @param mixed $secure + * @return void + */ public function validateSecure($secure) { return is_bool($secure); From 6374bb3dacd2ed2d769e5b3af588fdb0f369dfc4 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Fri, 4 May 2012 10:20:24 -0400 Subject: [PATCH 5/8] Removing a method that isn't available in sockets and fixing validation and tests --- .../Component/Server/FlashPolicyComponent.php | 160 +++--------------- .../Server/FlashPolicyComponentTest.php | 38 +---- 2 files changed, 35 insertions(+), 163 deletions(-) diff --git a/src/Ratchet/Component/Server/FlashPolicyComponent.php b/src/Ratchet/Component/Server/FlashPolicyComponent.php index aea3629..b31e813 100644 --- a/src/Ratchet/Component/Server/FlashPolicyComponent.php +++ b/src/Ratchet/Component/Server/FlashPolicyComponent.php @@ -18,7 +18,6 @@ class FlashPolicyComponent implements MessageComponentInterface { protected $_policy = ''; protected $_access = array(); - protected $_headers = array(); protected $_siteControl = ''; protected $_cache = ''; @@ -37,7 +36,7 @@ class FlashPolicyComponent implements MessageComponentInterface { public function onMessage(ConnectionInterface $from, $msg) { if (!$this->_cacheValid) { - $this->_cache = $this->renderPolicy()->asXML(); + $this->_cache = $this->renderPolicy()->asXML(); $this->_cacheValid = true; } @@ -66,26 +65,27 @@ class FlashPolicyComponent implements MessageComponentInterface { return new CloseConnection($conn); } - /** * setSiteControl function. - * + * * @access public * @param string $permittedCrossDomainPolicies (default: 'all') - * @return void + * @return bool */ public function setSiteControl($permittedCrossDomainPolicies = 'all') { if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { throw new \UnexpectedValueException('Invalid site control set'); + return false; } $this->_siteControl = $permittedCrossDomainPolicies; + return true; } /** * renderPolicy function. - * + * * @access public - * @return void + * @return SimpleXMLElement */ public function renderPolicy() { @@ -111,184 +111,80 @@ class FlashPolicyComponent implements MessageComponentInterface { $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; } /** * addAllowedAccess function. - * + * * @access public - * @param mixed $domain + * @param string $domain * @param string $ports (default: '*') * @param bool $secure (default: false) - * @return void + * @return bool */ public function addAllowedAccess($domain, $ports = '*', $secure = false) { if (!$this->validateDomain($domain)) { throw new \UnexpectedValueException('Invalid domain'); + return false; } if (!$this->validatePorts($ports)) { throw new \UnexpectedValueException('Invalid Port'); + return false; } $this->_access[] = array($domain, $ports, $secure); $this->_cacheValid = false; - } - /** - * addAllowedHTTPRequestHeaders function. - * - * @access public - * @param mixed $domain - * @param mixed $headers - * @param bool $secure (default: true) - * @return void - */ - 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; + return true; } /** * validateSiteControl function. - * + * * @access public * @param mixed $permittedCrossDomainPolicies * @return void */ public function validateSiteControl($permittedCrossDomainPolicies) { - return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'by-content-type', 'all')); + //'by-content-type' and 'by-ftp-filename' not available for sockets + return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'all')); } /** * validateDomain function. - * + * * @access public - * @param mixed $domain - * @return void + * @param string $domain + * @return bool */ 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); + return (bool)preg_match("/^((http(s)?:\/\/)?([a-z0-9-_]+\.|\*\.)*([a-z0-9-_\.]+)|\*)$/i", $domain); } - + /** * validatePorts function. - * + * * @access public - * @param mixed $port - * @return void + * @param string $port + * @return bool */ 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; - } - - /** - * validateHeaders function. - * - * @access public - * @param mixed $headers - * @return void - */ - 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; + return (bool)preg_match('/^(\*|(\d+[,-]?)*\d+)$/', $port); } /** * validateSecure function. - * + * * @access public - * @param mixed $secure - * @return void + * @param bool $secure + * @return bool */ public function validateSecure($secure) { diff --git a/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php index 16f6659..6382711 100644 --- a/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php +++ b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php @@ -18,19 +18,16 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { $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(); } @@ -38,7 +35,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { public function testInvalidDomainPolicyReader() { $this->setExpectedException('UnexpectedValueException'); $this->_policy->setSiteControl('all'); - $this->_policy->addAllowedHTTPRequestHeaders('*', '*'); $this->_policy->addAllowedAccess('dev.example.*', '*'); $this->_policy->renderPolicy(); } @@ -56,7 +52,7 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { array(true, 'all') , array(true, 'none') , array(true, 'master-only') - , array(true, 'by-content-type') + , array(false, 'by-content-type') , array(false, 'by-ftp-filename') , array(false, '') , array(false, 'all ') @@ -79,18 +75,20 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { array(true, '*') , array(true, 'example.com') , array(true, 'exam-ple.com') + , array(true, '*.exmple.com') , array(true, 'www.example.com') + , array(true, 'dev.dev.example.com') , array(true, 'http://example.com') + , array(true, 'https://example.com') , array(true, 'http://*.example.com') , array(false, 'exam*ple.com') - , array(true, '127.0.0.1') + , array(true, '127.0.255.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.*') ); } @@ -108,11 +106,11 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { , 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(false, '516-') , array(true, '516-523,11') + , array(false, '516,-523,11') , array(false, 'example') , array(false, 'asdf,123') , array(false, '--') @@ -121,28 +119,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { ); } - /** - * @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 */ From 52b1704155dc70bb9b9836f85a6d0ce8a4a2aa41 Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Mon, 7 May 2012 13:44:48 -0400 Subject: [PATCH 6/8] FlashPolicy all the things $policy = new Ratchet\Component\Server\FlashPolicyComponent(); $policy->addAllowedAccess('192.168.1.120', '8000'); $io = new Ratchet\Component\Server\IOServerComponent($policy); $io->run(843); --- .../Component/Server/FlashPolicyComponent.php | 190 +++++++++--------- .../Server/FlashPolicyComponentTest.php | 33 +-- 2 files changed, 95 insertions(+), 128 deletions(-) diff --git a/src/Ratchet/Component/Server/FlashPolicyComponent.php b/src/Ratchet/Component/Server/FlashPolicyComponent.php index b31e813..8510fac 100644 --- a/src/Ratchet/Component/Server/FlashPolicyComponent.php +++ b/src/Ratchet/Component/Server/FlashPolicyComponent.php @@ -4,6 +4,8 @@ use Ratchet\Component\MessageComponentInterface; use Ratchet\Resource\ConnectionInterface; use Ratchet\Resource\Connection; use Ratchet\Resource\Command\CommandInterface; +use Ratchet\Resource\Command\Action\SendMessage; +use Ratchet\Resource\Command\Action\CloseConnection; /** * An app to go on a server stack to pass a policy file to a Flash socket @@ -11,41 +13,102 @@ use Ratchet\Resource\Command\CommandInterface; * Be sure to run your server instance on port 843 * By default this lets accepts everything, make sure you tighten the rules up for production * @final - * @todo This just gets dumped with a whole xml file - I will make a nice API to implement this (eventually) - * @todo Move this into Ratchet when the above todo is complete + * @link http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html + * @link http://learn.adobe.com/wiki/download/attachments/64389123/CrossDomain_PolicyFile_Specification.pdf?version=1 + * @link view-source:http://www.adobe.com/xml/schemas/PolicyFileSocket.xsd */ class FlashPolicyComponent implements MessageComponentInterface { - protected $_policy = ''; - protected $_access = array(); + /** + * Contains the root policy node + * @var string + */ + protected $_policy = ''; + + /** + * Stores an array of allowed domains and their ports + * @var array + */ + protected $_access = array(); + + /** + * @var string + */ protected $_siteControl = ''; - protected $_cache = ''; + /** + * @var string + */ + protected $_cache = ''; + + /** + * @var string + */ protected $_cacheValid = false; /** - * @{inheritdoc} + * Add a domain to an allowed access list. + * + * @param string Specifies a requesting domain to be granted access. Both named domains and IP + * addresses are acceptable values. Subdomains are considered different domains. A wildcard (*) can + * be used to match all domains when used alone, or multiple domains (subdomains) when used as a + * prefix for an explicit, second-level domain name separated with a dot (.) + * @param string A comma-separated list of ports or range of ports that a socket connection + * is allowed to connect to. A range of ports is specified through a dash (-) between two port numbers. + * Ranges can be used with individual ports when separated with a comma. A single wildcard (*) can + * be used to allow all ports. + * @param bool + * @return FlashPolicyComponent */ - public function onOpen(ConnectionInterface $conn) { - $conn->PolicyRequest = ''; + 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, (boolean)$secure); + $this->_cacheValid = false; + + return $this; } /** - * @{inheritdoc} + * site-control defines the meta-policy for the current domain. A meta-policy specifies acceptable + * domain policy files other than the master policy file located in the target domain's root and named + * crossdomain.xml. + * + * @param string + * @return FlashPolicyComponent + */ + public function setSiteControl($permittedCrossDomainPolicies = 'all') { + if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { + throw new \UnexpectedValueException('Invalid site control set'); + } + + $this->_siteControl = $permittedCrossDomainPolicies; + $this->_cacheValid = false; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function onOpen(ConnectionInterface $conn) { + } + + /** + * {@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"); @@ -53,141 +116,76 @@ class FlashPolicyComponent implements MessageComponentInterface { } /** - * @{inheritdoc} + * {@inheritdoc} */ public function onClose(ConnectionInterface $conn) { } /** - * @{inheritdoc} + * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { return new CloseConnection($conn); } /** - * setSiteControl function. + * Builds the crossdomain file based on the template policy * - * @access public - * @param string $permittedCrossDomainPolicies (default: 'all') - * @return bool - */ - public function setSiteControl($permittedCrossDomainPolicies = 'all') { - if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { - throw new \UnexpectedValueException('Invalid site control set'); - return false; - } - $this->_siteControl = $permittedCrossDomainPolicies; - return true; - } - - /** - * renderPolicy function. - * - * @access public * @return SimpleXMLElement */ public function renderPolicy() { - $policy = new \SimpleXMLElement($this->_policy); - $siteControl = $policy->addChild('site-control'); if ($this->_siteControl == '') { - throw new \UnexpectedValueException('Where\'s my site control?'); + $this->setSiteControl(); } + $siteControl->addAttribute('permitted-cross-domain-policies', $this->_siteControl); - if (empty($this->_access)) { - throw new \UnexpectedValueException('Missing site access'); + throw new \UnexpectedValueException('You must add a domain through addAllowedAccess()'); } - foreach ($this->_access as $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'); + $tmp->addAttribute('secure', ($access[2] === true) ? 'true' : 'false'); } return $policy; - } /** - * addAllowedAccess function. + * Make sure the proper site control was passed * - * @access public - * @param string $domain - * @param string $ports (default: '*') - * @param bool $secure (default: false) + * @param string * @return bool */ - public function addAllowedAccess($domain, $ports = '*', $secure = false) { - - if (!$this->validateDomain($domain)) { - throw new \UnexpectedValueException('Invalid domain'); - return false; - } - if (!$this->validatePorts($ports)) { - throw new \UnexpectedValueException('Invalid Port'); - return false; - } - - - $this->_access[] = array($domain, $ports, $secure); - $this->_cacheValid = false; - - return true; - } - - /** - * validateSiteControl function. - * - * @access public - * @param mixed $permittedCrossDomainPolicies - * @return void - */ public function validateSiteControl($permittedCrossDomainPolicies) { - - //'by-content-type' and 'by-ftp-filename' not available for sockets + //'by-content-type' and 'by-ftp-filename' are not available for sockets return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'all')); } /** - * validateDomain function. + * Validate for proper domains (wildcards allowed) * - * @access public - * @param string $domain + * @param string * @return bool */ public function validateDomain($domain) { - return (bool)preg_match("/^((http(s)?:\/\/)?([a-z0-9-_]+\.|\*\.)*([a-z0-9-_\.]+)|\*)$/i", $domain); } /** - * validatePorts function. + * Make sure valid ports were passed * - * @access public - * @param string $port + * @param string * @return bool */ public function validatePorts($port) { - return (bool)preg_match('/^(\*|(\d+[,-]?)*\d+)$/', $port); } - - /** - * validateSecure function. - * - * @access public - * @param bool $secure - * @return bool - */ - 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 index 6382711..cdce9a7 100644 --- a/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php +++ b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php @@ -13,7 +13,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { $this->_policy = new FlashPolicyComponent(); } - public function testPolicyRender() { $this->_policy->setSiteControl('all'); $this->_policy->addAllowedAccess('example.com', '*'); @@ -25,12 +24,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { $this->setExpectedException('UnexpectedValueException'); $this->_policy->renderPolicy(); } - - public function testAnotherInvalidPolicyReader() { - $this->setExpectedException('UnexpectedValueException'); - $this->_policy->addAllowedAccess('dev.example.com', '*'); - $this->_policy->renderPolicy(); - } public function testInvalidDomainPolicyReader() { $this->setExpectedException('UnexpectedValueException'); @@ -38,8 +31,7 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { $this->_policy->addAllowedAccess('dev.example.*', '*'); $this->_policy->renderPolicy(); } - - + /** * @dataProvider siteControl */ @@ -62,7 +54,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { ); } - /** * @dataProvider URI */ @@ -92,7 +83,6 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { ); } - /** * @dataProvider ports */ @@ -118,25 +108,4 @@ class FlashPolicyComponentTest extends \PHPUnit_Framework_TestCase { , array(false, '838*') ); } - - /** - * @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 From dcbd5015524f69d5eb0a6770ca71491c3105905e Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Mon, 7 May 2012 13:52:07 -0400 Subject: [PATCH 7/8] Revert the lock file --- composer.lock | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 11c0a03..df67938 100644 --- a/composer.lock +++ b/composer.lock @@ -10,10 +10,6 @@ "package": "guzzle", "version": "2.0.2" }, - { - "package": "guzzle/guzzle", - "version": "v2.0.2" - }, { "package": "symfony/event-dispatcher", "version": "dev-master", @@ -28,7 +24,7 @@ { "package": "symfony/validator", "version": "dev-master", - "source-reference": "dac248b43b62d30023dd9b73ad7e5b7bc1128e5e" + "source-reference": "704f655d060b14475d7bd2a0b6d653c70f88218a" } ], "packages-dev": null, From ad0f51ab2c2f341e5145eb2487bce4eb3392662a Mon Sep 17 00:00:00 2001 From: Mike Almond Date: Mon, 7 May 2012 13:54:27 -0400 Subject: [PATCH 8/8] Lock file. --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index df67938..496e1fb 100644 --- a/composer.lock +++ b/composer.lock @@ -7,8 +7,8 @@ "source-reference": "1e0aa60d109c630d19543d999f12e2852ef8f932" }, { - "package": "guzzle", - "version": "2.0.2" + "package": "guzzle/guzzle", + "version": "v2.0.2" }, { "package": "symfony/event-dispatcher",