diff --git a/src/Ratchet/Component/Server/FlashPolicyComponent.php b/src/Ratchet/Component/Server/FlashPolicyComponent.php new file mode 100644 index 0000000..8510fac --- /dev/null +++ b/src/Ratchet/Component/Server/FlashPolicyComponent.php @@ -0,0 +1,191 @@ +'; + + /** + * Stores an array of allowed domains and their ports + * @var array + */ + protected $_access = array(); + + /** + * @var string + */ + protected $_siteControl = ''; + + /** + * @var string + */ + protected $_cache = ''; + + /** + * @var string + */ + protected $_cacheValid = false; + + /** + * 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 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; + } + + /** + * 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; + } + + $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); + } + + /** + * Builds the crossdomain file based on the template policy + * + * @return SimpleXMLElement + */ + public function renderPolicy() { + $policy = new \SimpleXMLElement($this->_policy); + + $siteControl = $policy->addChild('site-control'); + + if ($this->_siteControl == '') { + $this->setSiteControl(); + } + + $siteControl->addAttribute('permitted-cross-domain-policies', $this->_siteControl); + + if (empty($this->_access)) { + throw new \UnexpectedValueException('You must add a domain through addAllowedAccess()'); + } + + 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'); + } + + return $policy; + } + + /** + * Make sure the proper site control was passed + * + * @param string + * @return bool + */ + public function validateSiteControl($permittedCrossDomainPolicies) { + //'by-content-type' and 'by-ftp-filename' are not available for sockets + return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'all')); + } + + /** + * Validate for proper domains (wildcards allowed) + * + * @param string + * @return bool + */ + public function validateDomain($domain) { + return (bool)preg_match("/^((http(s)?:\/\/)?([a-z0-9-_]+\.|\*\.)*([a-z0-9-_\.]+)|\*)$/i", $domain); + } + + /** + * Make sure valid ports were passed + * + * @param string + * @return bool + */ + public function validatePorts($port) { + return (bool)preg_match('/^(\*|(\d+[,-]?)*\d+)$/', $port); + } +} \ No newline at end of file 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()); diff --git a/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php new file mode 100644 index 0000000..cdce9a7 --- /dev/null +++ b/tests/Ratchet/Tests/Component/Server/FlashPolicyComponentTest.php @@ -0,0 +1,111 @@ +_policy = new FlashPolicyComponent(); + } + + public function testPolicyRender() { + $this->_policy->setSiteControl('all'); + $this->_policy->addAllowedAccess('example.com', '*'); + $this->_policy->addAllowedAccess('dev.example.com', '*'); + $this->assertInstanceOf('SimpleXMLElement', $this->_policy->renderPolicy()); + } + + public function testInvalidPolicyReader() { + $this->setExpectedException('UnexpectedValueException'); + $this->_policy->renderPolicy(); + } + + public function testInvalidDomainPolicyReader() { + $this->setExpectedException('UnexpectedValueException'); + $this->_policy->setSiteControl('all'); + $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(false, '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, '*.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.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') + ); + } + + /** + * @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(true, '507,516-523,333') + , array(true, '507,516-523,507,516-523') + , array(false, '516-') + , array(true, '516-523,11') + , array(false, '516,-523,11') + , array(false, 'example') + , array(false, 'asdf,123') + , array(false, '--') + , array(false, ',,,') + , array(false, '838*') + ); + } +} \ No newline at end of file