diff options
author | Josh Hoyt <josh@janrain.com> | 2006-01-27 20:50:33 +0000 |
---|---|---|
committer | Josh Hoyt <josh@janrain.com> | 2006-01-27 20:50:33 +0000 |
commit | 4db92f29d3fff6c6e8b39be1a0ae7350a3825edf (patch) | |
tree | 646ad081f635c96a3ed3ba621bee02f4a29702e3 | |
parent | 2e59e5b31e37d6f68eb3ab096ae068db1ea88e24 (diff) | |
download | php-openid-4db92f29d3fff6c6e8b39be1a0ae7350a3825edf.zip php-openid-4db92f29d3fff6c6e8b39be1a0ae7350a3825edf.tar.gz php-openid-4db92f29d3fff6c6e8b39be1a0ae7350a3825edf.tar.bz2 |
[project @ Added trust root handling code and tests]
-rw-r--r-- | Auth/OpenID/TrustRoot.php | 193 | ||||
-rw-r--r-- | Tests/Auth/OpenID/TrustRoot.php | 171 | ||||
-rw-r--r-- | Tests/Auth/OpenID/data/trustroot.txt | 115 | ||||
-rw-r--r-- | Tests/TestDriver.php | 3 |
4 files changed, 481 insertions, 1 deletions
diff --git a/Auth/OpenID/TrustRoot.php b/Auth/OpenID/TrustRoot.php new file mode 100644 index 0000000..66a73ec --- /dev/null +++ b/Auth/OpenID/TrustRoot.php @@ -0,0 +1,193 @@ +<?php + +/** + * Functions for dealing with OpenID trust roots + */ + +/** + * Parse a URL into its trust_root parts. + * + * @param string $trust_root: The url to parse + * + * @return mixed $parsed: Either an associative array of trust root + * parts or false if parsing failed. + */ +function Auth_OpenID___normalizeTrustRoot($trust_root) +{ + $parts = @parse_url($trust_root); + if ($parts === false) { + return false; + } + $required_parts = array('scheme', 'host'); + $forbidden_parts = array('user', 'pass', 'fragment'); + $keys = array_keys($parts); + if (array_intersect($keys, $required_parts) != $required_parts) { + return false; + } + + if (array_intersect($keys, $forbidden_parts) != array()) { + return false; + } + + $scheme = strtolower($parts['scheme']); + $allowed_schemes = array('http', 'https'); + if (!in_array($scheme, $allowed_schemes)) { + return false; + } + $parts['scheme'] = $scheme; + + $host = strtolower($parts['host']); + $hostparts = explode('*', $host); + switch (count($hostparts)) { + case 1: + $parts['wildcard'] = false; + break; + case 2: + if ($hostparts[0] || + ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) { + return false; + } + $host = $hostparts[1]; + $parts['wildcard'] = true; + break; + default: + return false; + } + if (strpos($host, ':') !== false) { + return false; + } + $parts['host'] = $host; + + if (isset($parts['path'])) { + $path = strtolower($parts['path']); + if (substr($path, -1) != '/') { + $path .= '/'; + } + } else { + $path = '/'; + } + $parts['path'] = $path; + if (!isset($parts['port'])) { + $parts['port'] = false; + } + return $parts; +} + +define('Auth_OpenID___TLDs', + '/\.(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|ac|' . + 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|' . + 'bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|' . + 'cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|' . + 'fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|' . + 'gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|' . + 'ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|' . + 'ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|' . + 'nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|' . + 'ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|' . + 'so|sr|st|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz|' . + 'ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)$/'); + +/** + * Is this trust root sane? + * + * A trust root is sane if it is syntactically valid and it has a + * reasonable domain name. Specifically, the domain name must be more + * than one level below a standard TLD or more than two levels below a + * two-letter tld. + * + * For example, '*.com' is not a sane trust root, but '*.foo.com' is. + * '*.co.uk' is not sane, but '*.bbc.co.uk' is. + * + * This check is not always correct, but it attempts to err on the + * side of marking sane trust roots insane instead of marking insane + * trust roots sane. For example, 'kink.fm' is marked as insane even + * though it "should" (for some meaning of should) be marked sane. + * + * This function should be used when creating OpenID servers to alert + * the users of the server when a consumer attempts to get the user to + * accept a suspicious trust root. + * + * @param string $tr: The trust root to check + * + * @return bool $sanity: Whether the trust root looks OK + */ +function Auth_OpenID_saneTrustRoot($tr) +{ + $parts = Auth_OpenID___normalizeTrustRoot($tr); + if ($parts === false) { + return false; + } + if ($parts['host'] == 'localhost') { + return true; + } + preg_match(Auth_OpenID___TLDs, $parts['host'], $matches); + if (!$matches) { + return false; + } + $tld = $matches[1]; + $elements = explode('.', $parts['host']); + $n = count($elements); + if ($parts['wildcard']) { + $n -= 1; + } + if (strlen($tld) == 2) { + $n -= 1; + } + if ($n <= 1) { + return false; + } + return true; +} + +/** + * Does this URL match the given trust root? + * + * Return whether the URL falls under the given trust root. This does + * not check whether the trust root is sane. If the URL or trust root + * do not parse, this function will return false. + * + * @param string $trust_root: The trust root to match against + * + * @param string $url: The URL to check + * + * @return bool $matches: Whether the URL matches against the trust root + */ +function Auth_OpenID_matchTrustRoot($trust_root, $url) +{ + $trust_root_parsed = Auth_OpenID___normalizeTrustRoot($trust_root); + $url_parsed = Auth_OpenID___normalizeTrustRoot($url); + if (!$trust_root_parsed || !$url_parsed) { + return false; + } + + // Check hosts matching + if ($url_parsed['wildcard']) { + return false; + } + if ($trust_root_parsed['wildcard']) { + $host_tail = $trust_root_parsed['host']; + $host = $url_parsed['host']; + if ($host_tail && + substr($host, -(strlen($host_tail))) != $host_tail && + substr($host_tail, 1) != $host) { + return false; + } + } else { + if ($trust_root_parsed['host'] != $url_parsed['host']) { + return false; + } + } + + // Check path matching + $base_path = $trust_root_parsed['path']; + $path = $url_parsed['path']; + if (substr($path, 0, strlen($base_path)) != $base_path) { + return false; + } + + // The port and scheme need to match exactly + return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] && + $url_parsed['port'] === $trust_root_parsed['port']); +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/TrustRoot.php b/Tests/Auth/OpenID/TrustRoot.php new file mode 100644 index 0000000..54694d0 --- /dev/null +++ b/Tests/Auth/OpenID/TrustRoot.php @@ -0,0 +1,171 @@ +<?php + +/** + * Tests for the TrustRoot module + */ + +require_once "Auth/OpenID/TrustRoot.php"; +require_once "Tests/Auth/OpenID/Util.php"; +require_once "PHPUnit.php"; + +class Tests_Auth_OpenID_TRParseCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_TRParseCase($desc, $case, $expected) + { + $this->setName($desc); + $this->case = $case; + $this->expected = $expected; + } + + function runTest() + { + $is_sane = Auth_OpenID_saneTrustRoot($this->case); + $parsed = (bool)Auth_OpenID___normalizeTrustRoot($this->case); + switch ($this->expected) { + case 'sane': + $this->assertTrue($is_sane); + $this->assertTrue($parsed); + break; + case 'insane': + $this->assertTrue($parsed); + $this->assertFalse($is_sane); + break; + default: + $this->assertFalse($parsed); + $this->assertFalse($is_sane); + } + } +} + +class Tests_Auth_OpenID_TRMatchCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_TRMatchCase($desc, $tr, $rt, $matches) + { + $this->setName($desc); + $this->tr = $tr; + $this->rt = $rt; + $this->matches = $matches; + } + + function runTest() + { + $matches = Auth_OpenID_matchTrustRoot($this->tr, $this->rt); + if ($this->matches && !$matches) { + var_export(Auth_OpenID___normalizeTrustRoot($this->tr)); + var_export(Auth_OpenID___normalizeTrustRoot($this->rt)); + } + + $this->assertEquals((bool)$this->matches, (bool)$matches); + } +} + +function Tests_Auth_OpenID_parseHeadings($data, $c) +{ + $heading_pat = '/(^|\n)' . $c . '{40}\n([^\n]+)\n' . $c . '{40}\n()/'; + $offset = 0; + $headings = array(); + while (true) { + preg_match($heading_pat, $data, $matches, PREG_OFFSET_CAPTURE, $offset); + if (!$matches) { + break; + } + $start = $matches[0][1]; + $heading = $matches[2][0]; + $end = $matches[3][1]; + $headings[] = array('heading' => $heading, + 'start' => $start, + 'end' => $end, + ); + $offset = $end; + } + return $headings; +} + +function Tests_Auth_OpenID_getSections($data) +{ + $headings = Tests_Auth_OpenID_parseHeadings($data, '-'); + $sections = array(); + $n = count($headings); + for ($i = 0; $i < $n; ) { + $secdata = $headings[$i]; + list($numtests, $desc) = explode(': ', $secdata['heading']); + $start = $secdata['end']; + $i += 1; + if ($i < $n) { + $blob = substr($data, $start, $headings[$i]['start'] - $start); + } else { + $blob = substr($data, $start); + } + $lines = explode("\n", trim($blob)); + if (count($lines) != $numtests) { + trigger_error('Parse failure: ' . var_export($secdata, true), + E_USER_ERROR); + } + $sections[] = array('desc' => $desc, 'lines' => $lines,); + } + return $sections; +} + +function Tests_Auth_OpenID_trParseTests($head, $tests) +{ + $tests = array('fail' => $tests[0], + 'insane' => $tests[1], + 'sane' => $tests[2]); + $testobjs = array(); + foreach ($tests as $expected => $testdata) { + $lines = $testdata['lines']; + foreach ($lines as $line) { + $desc = sprintf("%s - %s: %s", $head, + $testdata['desc'], var_export($line, true)); + $testobjs[] = new Tests_Auth_OpenID_TRParseCase( + $desc, $line, $expected); + } + } + return $testobjs; +} + +function Tests_Auth_OpenID_trMatchTests($head, $tests) +{ + $tests = array(true => $tests[0], false => $tests[1]); + $testobjs = array(); + foreach ($tests as $expected => $testdata) { + $lines = $testdata['lines']; + foreach ($lines as $line) { + $pat = '/^([^ ]+) +([^ ]+)$/'; + preg_match($pat, $line, $matches); + list($_, $tr, $rt) = $matches; + $desc = sprintf("%s - %s: %s %s", $head, $testdata['desc'], + var_export($tr, true), var_export($rt, true)); + $testobjs[] = new Tests_Auth_OpenID_TRMatchCase( + $desc, $tr, $rt, $expected); + } + } + return $testobjs; +} + +function Tests_Auth_OpenID_trustRootTests() +{ + $data = Tests_Auth_OpenID_readdata('trustroot.txt'); + list($parsehead, $matchhead) = Tests_Auth_OpenID_parseHeadings($data, '='); + $pe = $parsehead['end']; + $parsedata = substr($data, $pe, $matchhead['start'] - $pe); + $parsetests = Tests_Auth_OpenID_getSections($parsedata); + $parsecases = Tests_Auth_OpenID_trParseTests($parsehead['heading'], + $parsetests); + + $matchdata = substr($data, $matchhead['end']); + $matchtests = Tests_Auth_OpenID_getSections($matchdata); + $matchcases = Tests_Auth_OpenID_trMatchTests($matchhead['heading'], + $matchtests); + + return array_merge($parsecases, $matchcases); +} + +class Tests_Auth_OpenID_TrustRoot extends PHPUnit_TestSuite { + function Tests_Auth_OpenID_TrustRoot($name) + { + $this->setName($name); + + foreach (Tests_Auth_OpenID_trustRootTests() as $test) { + $this->addTest($test); + } + } +}
\ No newline at end of file diff --git a/Tests/Auth/OpenID/data/trustroot.txt b/Tests/Auth/OpenID/data/trustroot.txt new file mode 100644 index 0000000..0db7f50 --- /dev/null +++ b/Tests/Auth/OpenID/data/trustroot.txt @@ -0,0 +1,115 @@ +======================================== +Trust root parsing checking +======================================== + +---------------------------------------- +14: Does not parse +---------------------------------------- +baz.org +*.foo.com +http://*.schtuff.*/ +ftp://foo.com +ftp://*.foo.com +http://*.foo.com:80:90/ +foo.*.com +http://foo.*.com +http://www.* +http://*foo.com/ + + + +5 + +---------------------------------------- +12: Insane +---------------------------------------- +http://*/ +https://*/ +http://*.com +http://*.com/ +https://*.com/ +http://*.com.au/ +http://*.co.uk/ +http://*.foo.notatld/ +https://*.foo.notatld/ +http://*.museum/ +https://*.museum/ +http://kink.fm/should/be/sane + +---------------------------------------- +14: Sane +---------------------------------------- +http://*.schtuff.com/ +http://*.foo.schtuff.com/ +http://*.schtuff.com +http://www.schtuff.com/ +http://www.schutff.com +http://*.this.that.schtuff.com/ +http://*.foo.com/path +http://*.foo.com/path?action=foo2 +http://x.foo.com/path?action=foo2 +http://x.foo.com/path?action=%3D +http://localhost:8081/ +http://localhost:8082/?action=openid +https://foo.com/ +http://goathack.livejournal.org:8020/openid/login.bml + +======================================== +return_to matching +======================================== + +---------------------------------------- +29: matches +---------------------------------------- +http://*/ http://cnn.com/ +http://*/ http://livejournal.com/ +http://*/ http://met.museum/ +http://*:8081/ http://met.museum:8081/ +http://localhost:8081/x?action=openid http://localhost:8081/x?action=openid +http://*.foo.com http://b.foo.com +http://*.foo.com http://b.foo.com/ +http://*.foo.com/ http://b.foo.com +http://b.foo.com http://b.foo.com +http://b.foo.com http://b.foo.com/ +http://b.foo.com/ http://b.foo.com +http://*.b.foo.com http://b.foo.com +http://*.b.foo.com http://b.foo.com/ +http://*.b.foo.com/ http://b.foo.com +http://*.b.foo.com http://x.b.foo.com +http://*.b.foo.com http://w.x.b.foo.com +http://*.bar.co.uk http://www.bar.co.uk +http://*.uoregon.edu http://x.cs.uoregon.edu +http://x.com/abc http://x.com/abc +http://x.com/abc http://x.com/abc/def +http://*.x.com http://x.com/gallery +http://*.x.com http://foo.x.com/gallery +http://foo.x.com http://foo.x.com/gallery/xxx +http://*.x.com/gallery http://foo.x.com/gallery +http://localhost:8082/?action=openid http://localhost:8082/?action=openid +http://goathack.livejournal.org:8020/ http://goathack.livejournal.org:8020/openid/login.bml +https://foo.com https://foo.com +http://Foo.com http://foo.com +http://foo.com http://Foo.com + +---------------------------------------- +19: does not match +---------------------------------------- +http://*/ ftp://foo.com/ +http://*/ xxx +http://*.x.com/abc http://foo.x.com +http://*.x.com/abc http://*.x.com +http://*.com/ http://*.com/ +http://x.com/abc http://x.com/ +http://x.com/abc http://x.com/a +http://x.com/abc http://x.com/ab +http://x.com/abc http://x.com/abcd +http://*.cs.uoregon.edu http://x.uoregon.edu +http://*.foo.com http://bar.com +http://*.foo.com http://www.bar.com +http://*.bar.co.uk http://xxx.co.uk +https://foo.com http://foo.com +http://foo.com https://foo.com +http://foo.com:80 http://foo.com +http://foo.com http://foo.com:80 +http://foo.com:81 http://foo.com:80 +http://*:80 http://foo.com:81 diff --git a/Tests/TestDriver.php b/Tests/TestDriver.php index b8a1dc4..cd7faf8 100644 --- a/Tests/TestDriver.php +++ b/Tests/TestDriver.php @@ -74,7 +74,8 @@ $_test_names = array( 'OIDUtil', 'Parse', 'StoreTest', - 'Server' + 'Server', + 'TrustRoot', ); function selectTests($names) |