From 5aa5f3bbb63256a942c9528d18cf61b3e07c10f8 Mon Sep 17 00:00:00 2001 From: Olav Morken <olav.morken@uninett.no> Date: Tue, 12 Feb 2008 13:50:17 +0000 Subject: [PATCH] Added test script as bin/test.php. It is configured in config/test.php. git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@283 44740490-163a-0410-bde0-09ae8108e29a --- bin/test.php | 608 +++++++++++++++++++++++++++++++++++++++ config/test-template.php | 55 ++++ 2 files changed, 663 insertions(+) create mode 100755 bin/test.php create mode 100644 config/test-template.php diff --git a/bin/test.php b/bin/test.php new file mode 100755 index 000000000..a6ac23581 --- /dev/null +++ b/bin/test.php @@ -0,0 +1,608 @@ +#!/usr/bin/env php +<?php + +/* + * This script can be used to test a login-logout sequence to a specific IdP. + * It is configured from the config/test.php file. A template for that file + * can be found in config/test-template.php. + */ + +$tests = array(); + +require_once('../config/test.php'); + +/** + * This function creates a curl handle and initializes it. + * + * @return A curl handler. + */ +function curlCreate() { + + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); + curl_setopt($ch, CURLOPT_COOKIEFILE, ''); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + + return $ch; +} + + +/** + * This function requests a url with a GET request. + * + * @param $curl The curl handle which should be used. + * @param $url The url which should be requested. + * @param $parameters Associative array with parameters which should be appended to the url. + * @return The content of the returned page. + */ +function urlGet($curl, $url, $parameters = array()) { + + $p = ''; + foreach($parameters as $k => $v) { + if($p != '') { + $p .= '&'; + } + + $p .= urlencode($k) . '=' . urlencode($v); + } + + if(strpos($url, '?') === FALSE) { + $url .= '?' . $p; + } else { + $url .= '&' . $p; + } + + curl_setopt($curl, CURLOPT_HTTPGET, TRUE); + curl_setopt($curl, CURLOPT_URL, $url); + + $curl_scraped_page = curl_exec($curl); + if($curl_scraped_page === FALSE) { + echo('Failed to get url: ' . $url . "\n"); + echo('Curl error: ' . curl_error($curl) . "\n"); + return FALSE; + } + + return $curl_scraped_page; +} + + +/** + * This function posts data to a specific url. + * + * @param $curl The curl handle which should be used for the request. + * @param $url The url the POST request should be directed to. + * @param $post Associative array with the post parameters. + * $return The returned page. + */ +function urlPost($curl, $url, $post) { + + $postparams = ''; + + foreach($post as $k => $v) { + if($postparams != '') { + $postparams .= '&'; + } + + $postparams .= urlencode($k) . '=' . urlencode($v); + } + + + curl_setopt($curl, CURLOPT_POSTFIELDS, $postparams); + curl_setopt($curl, CURLOPT_POST, TRUE); + curl_setopt($curl, CURLOPT_URL, $url); + + $curl_scraped_page = curl_exec($curl); + if($curl_scraped_page === FALSE) { + echo('Failed to get url: ' . $url . "\n"); + echo('Curl error: ' . curl_error($curl) . "\n"); + return FALSE; + } + + return $curl_scraped_page; +} + + +/** + * This function parses a simpleSAMLphp HTTP-REDIRECT debug page. + * + * @param $page The content of the page. + * @return FALSE if $page isn't a HTTP-REDIRECT debug page, destination url if it is. + */ +function parseSimpleSamlHttpRedirectDebug($page) { + if(strpos($page, '<h2>Sending a SAML message using HTTP-REDIRECT</h2>') === FALSE) { + return FALSE; + } + + if(!preg_match('/<a id="sendlink" href="([^"]*)">send SAML message<\\/a>/', $page, $matches)) { + echo('Invalid simpleSAMLphp debug page. Missing link.' . "\n"); + return FALSE; + } + + $url = $matches[1]; + $url = html_entity_decode($url); + + return $url; +} + + +/** + * This function parses a simpleSAMLphp HTTP-POST page. + * + * @param $page The content of the page. + * @return FALSE if $page isn't a HTTP-POST page. If it is a HTTP-POST page, it will return an associative array with + * the post destination in 'url' and the post arguments as an associative array in 'post'. + */ +function parseSimpleSamlHttpPost($page) { + if(strpos($page, '<title>SAML 2.0 POST</title>') === FALSE && strpos($page, '<title>SAML Response Debug-mode</title>') === FALSE) { + return FALSE; + } + + if(!preg_match('/<form method="post" action="([^"]*)">/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing form target.' . "\n"); + return FALSE; + } + $url = html_entity_decode($matches[1]); + + if(!preg_match('/<input type="hidden" name="SAMLResponse" value="([^"]*)" \\/>/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing SAMLResponse.' . "\n"); + return FALSE; + } + $samlResponse = html_entity_decode($matches[1]); + + if(!preg_match('/<input type="hidden" name="RelayState" value="([^"]*)" \\/>/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing RelayState.' . "\n"); + return FALSE; + } + $relayState = html_entity_decode($matches[1]); + + + return array('url' => $url, 'post' => array('SAMLResponse' => $samlResponse, 'RelayState' => $relayState)); +} + + +/** + * This function parses a simpleSAMLphp HTTP-POST debug page. + * + * @param $page The content of the page. + * @return FALSE if $page isn't a HTTP-POST page. If it is a HTTP-POST page, it will return an associative array with + * the post destination in 'url' and the post arguments as an associative array in 'post'. + */ +function parseSimpleSamlHttpPostDebug($page) { + if(strpos($page, '<title>SAML Response Debug-mode</title>') === FALSE) { + return FALSE; + } + + if(!preg_match('/<form method="post" action="([^"]*)">/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing form target.' . "\n"); + return FALSE; + } + $url = html_entity_decode($matches[1]); + + if(!preg_match('/<input type="hidden" name="SAMLResponse" value="([^"]*)" \\/>/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing SAMLResponse.' . "\n"); + return FALSE; + } + $samlResponse = html_entity_decode($matches[1]); + + if(!preg_match('/<input type="hidden" name="RelayState" value="([^"]*)" \\/>/', $page, $matches)) { + echo('Invalid simpleSAMLphp HTTP-POST page. Missing RelayState.' . "\n"); + return FALSE; + } + $relayState = html_entity_decode($matches[1]); + + + return array('url' => $url, 'post' => array('SAMLResponse' => $samlResponse, 'RelayState' => $relayState)); +} + + +/** + * This function parses a simpleSAMLphp login page. + * + * @param $curl The curl handle the page was fetched with. + * @param $page The content of the login page. + * @return FALSE if $page isn't a login page, associative array with the destination url in 'url' and the relaystate in 'relaystate'. + */ +function parseSimpleSamlLoginPage($curl, $page) { + + $url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + + $pos = strpos($url, '?'); + if($pos === FALSE) { + echo('Unexpected login page url: ' . $url); + return FALSE; + } + $url = substr($url, 0, $pos + 1); + + + if(!preg_match('/<input type="hidden" name="RelayState" value="([^"]*)" \\/>/', $page, $matches)) { + echo('Could not find relaystate in simpleSAMLphp login page.' . "\n"); + return FALSE; + } + + $relaystate = $matches[1]; + $relaystate = html_entity_decode($relaystate); + + return array('url' => $url, 'relaystate' => $relaystate); +} + + +/** + * This function parses a FEIDE login page. + * + * @param $curl The curl handle the page was fetched with. + * @param $page The content of the login page. + * @return FALSE if $page isn't a login page, associative array with the destination url in 'url' and the goto attribute in 'goto'. + */ +function parseFeideLoginPage($curl, $page) { + + if(strpos($page, '<title> Moria-innlogging </title>') === FALSE) { + echo('Not a FEIDE login page.' . "\n"); + return FALSE; + } + + $url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + + $pos = strpos($url, '/amserver/UI/Login'); + if($pos === FALSE) { + echo('Unexpected login page url: ' . $url); + return FALSE; + } + $url = substr($url, 0, $pos) . '/amserver/UI/Login'; + + if(!preg_match('/<input type="hidden" name="goto" value="([^"]*)"\\/>/', $page, $matches)) { + echo('Could not find goto in FEIDE login page.' . "\n"); + return FALSE; + } + + $goto = $matches[1]; + $goto = html_entity_decode($goto); + + return array('url' => $url, 'goto' => $goto); +} + + +/** + * This function parses the FEIDE HTTP-POST page. + * + * @param $page The content of the page. + * @return FALSE if $page isn't a HTTP-POST page. If it is a HTTP-POST page, it will return an associative array with + * the post destination in 'url' and the post arguments as an associative array in 'post'. + */ +function parseFeideHttpPost($page) { + + if(strpos($page, '<TITLE>Access rights validated</TITLE>') === FALSE) { + return FALSE; + } + + if(!preg_match('/<FORM METHOD="POST" ACTION="([^"]*)">/', $page, $matches)) { + echo('Invalid FEIDE HTTP-POST page. Missing form target.' . "\n"); + return FALSE; + } + $url = html_entity_decode($matches[1]); + + if(!preg_match('/<INPUT TYPE="HIDDEN" NAME="SAMLResponse" VALUE="([^"]*)">/m', $page, $matches)) { + echo('Invalid FEIDE HTTP-POST page. Missing SAMLResponse.' . "\n"); + return FALSE; + } + $samlResponse = html_entity_decode($matches[1]); + + if(!preg_match('/<INPUT TYPE="HIDDEN" NAME="RelayState" VALUE="([^"]*)">/', $page, $matches)) { + echo('Invalid FEIDE HTTP-POST page. Missing RelayState.' . "\n"); + return FALSE; + } + $relayState = html_entity_decode($matches[1]); + + + return array('url' => $url, 'post' => array('SAMLResponse' => $samlResponse, 'RelayState' => $relayState)); +} + + +/** + * This function handles simpleSAMLphp debug pages, and follows redirects in them. + * + * @param $curl The curl handle we should use. + * @param $page The page which may be a simpleSAMLphp debug page. $page may be FALSE, in which case this function + * will return FALSE. + * @return $page if $page isn't a debug page, or the result from following the redirect if not. + * FALSE will be returned on failure. + */ +function skipDebugPage($curl, $page) { + + if($page === FALSE) { + return FALSE; + } + + $url = parseSimpleSamlHttpRedirectDebug($page); + if($url !== FALSE) { + $page = urlGet($curl, $url); + } + + return $page; +} + + +/** + * This function contacts the test page to initialize SSO. + * + * @param $test The test we are running. + * @param $curl The curl handle we should use. + * @return TRUE on success, FALSE on failure. + */ +function initSSO($test, $curl) { + if(!array_key_exists('url', $test)) { + echo('Missing required attribute url in test.' . "\n"); + return FALSE; + } + + $params = array('op' => 'login'); + if(array_key_exists('idp', $test)) { + $params['idp'] = $test['idp']; + } + + /* Add attribute tests. */ + if(array_key_exists('attributes', $test)) { + $i = 0; + foreach($test['attributes'] as $name => $values) { + if(!is_array($values)) { + $values = array($values); + } + + foreach($values as $value) { + $params['attr_test_' . $i] = $name . ':' . $value; + $i++; + } + } + } + + echo('Initializing SSO.' . "\n"); + $loginPage = urlGet($curl, $test['url'], $params); + if($loginPage === FALSE) { + echo('Failed to initialize SSO.' . "\n"); + return FALSE; + } + + /* Skip HTTP-REDIRECT debug page if it appears. */ + $loginPage = skipDebugPage($curl, $loginPage); + + return $loginPage; +} + + +/** + * This function handles login to a simpleSAMLphp login page. + * + * @param $test The current test. + * @param $curl The curl handle in use. + * @param $page The login page. + * @return FALSE on failure, or the resulting page on success. + */ +function doSimpleSamlLogin($test, $curl, $page) { + + if(!array_key_exists('username', $test)) { + echo('Missing username in test.' . "\n"); + return FALSE; + } + + if(!array_key_exists('password', $test)) { + echo('Missing password in test.' . "\n"); + return FALSE; + } + + $info = parseSimpleSamlLoginPage($curl, $page); + if($info === FALSE) { + return FALSE; + } + + $post = array(); + $post['username'] = $test['username']; + $post['password'] = $test['password']; + $post['RelayState'] = $info['relaystate']; + + $page = urlPost($curl, $info['url'], $post); + + + /* Follow HTTP-POST redirect. */ + $pi = parseSimpleSamlHttpPost($page); + if($pi === FALSE) { + echo($page); + echo('Didn\'t get a simpleSAMLphp post redirect page.' . "\n"); + return FALSE; + } + + $page = urlPost($curl, $pi['url'], $pi['post']); + + return $page; +} + + +/** + * This function handles login to the FEIDE login page. + * + * @param $test The current test. + * @param $curl The curl handle in use. + * @param $page The login page. + * @return FALSE on failure, or the resulting page on success. + */ +function doFeideLogin($test, $curl, $page) { + + if(!array_key_exists('username', $test)) { + echo('Missing username in test.' . "\n"); + return FALSE; + } + + if(!array_key_exists('password', $test)) { + echo('Missing password in test.' . "\n"); + return FALSE; + } + + if(!array_key_exists('organization', $test)) { + echo('Missing organization in test.' . "\n"); + return FALSE; + } + + + $info = parseFeideLoginPage($curl, $page); + if($info === FALSE) { + return FALSE; + } + + $post = array(); + $post['username'] = $test['username']; + $post['password'] = $test['password']; + $post['organization'] = $test['organization']; + $post['goto'] = $info['goto']; + + $page = urlPost($curl, $info['url'], $post); + + + /* Follow HTTP-POST redirect. */ + $pi = parseFeideHttpPost($page); + if($pi === FALSE) { + echo('Unable to parse FEIDE HTTP-POST redirect page.' . "\n"); + return FALSE; + } + + $page = urlPost($curl, $pi['url'], $pi['post']); + + return $page; +} + +/** + * This function logs in using the configuration of the given test. + * + * @param $test The current test. + * @param $curl The curl handle in use. + * @param $page The login page. + * @return FALSE on failure, or the resulting page on success. + */ +function doLogin($test, $curl, $page) { + if(!array_key_exists('logintype', $test)) { + echo('Missing option \'logintype\' in test configuration.' . "\n"); + return FALSE; + } + + switch($test['logintype']) { + case 'simplesaml': + return doSimpleSamlLogin($test, $curl, $page); + case 'feide': + return doFeideLogin($test, $curl, $page); + default: + echo('Unknown login type: ' . $test['logintype'] . "\n"); + echo($page); + return FALSE; + } +} + + +/** + * This function contacts the test page to initialize SSO. + * + * @param $test The test we are running. + * @param $curl The curl handle we should use. + * @return TRUE on success, FALSE on failure. + */ +function doLogout($test, $curl) { + if(!array_key_exists('url', $test)) { + echo('Missing required attribute url in test.' . "\n"); + return FALSE; + } + + $params = array('op' => 'logout'); + + $page = urlGet($curl, $test['url'], $params); + if($page === FALSE) { + echo('Failed to log out.' . "\n"); + return FALSE; + } + + /* Skip HTTP-REDIRECT debug pagess. */ + while(TRUE) { + $newPage = skipDebugPage($curl, $page); + if($newPage === $page) { + break; + } + $page = $newPage; + } + + return $page; +} + + +/** + * This function runs the specified test. + * + * @param $test Associative array with the test parameters. + * @return TRUE on success, FALSE on failure. + */ +function doTest($test) { + $curl = curlCreate(); + + /* Initialize SSO. */ + do { + $loginPage = initSSO($test, $curl); + if($loginPage === FALSE) { + $res = FALSE; + break; + } + + echo('Logging in.' . "\n"); + + $result = doLogin($test, $curl, $loginPage); + if($result !== "OK") { + if(is_string($result)) { + echo('Failed to log in. Result from SP: ' . $result . "\n"); + } else { + echo('Failed to log in.' . "\n"); + } + $res = FALSE; + break; + } + + echo('Logged in, attributes OK' . "\n"); + + echo('Logging out.' . "\n"); + + $result = doLogout($test, $curl); + if($result !== "OK") { + if(is_string($result)) { + echo('Failed to log out. Result from SP: ' . $result . "\n"); + } else { + echo('Failed to log out.' . "\n"); + } + $res = FALSE; + break; + } + + echo('Logged out.' . "\n"); + + } while(0); + + curl_close($curl); + + return $res; +} + + +$ret = 0; +/* Run the tests. */ +foreach($tests as $i => $test) { + echo('############################################################' . "\n"); + echo('Running test #' . ($i + 1) . '.' . "\n"); + + $res = doTest($test); + + if($res === FALSE) { + $ret = 1; + echo('Test #' . ($i + 1) . ' failed.' . "\n"); + } else { + echo('Test #' . ($i + 1) . ' succeeded.' . "\n"); + } +} +echo('############################################################' . "\n"); + +exit($ret); + +?> \ No newline at end of file diff --git a/config/test-template.php b/config/test-template.php new file mode 100644 index 000000000..76d2ae650 --- /dev/null +++ b/config/test-template.php @@ -0,0 +1,55 @@ +<?php + +/* + * This is the configuration file for the test script which can be found at + * bin/test.php. + * + */ + +/* Add a test towards the default IdP using the simpleSAMLphp login handler. */ +$tests[] = array( + + /* The full url to the admin/test.php page on the SP. */ + 'url' => 'https://example.org/simplesaml/admin/test.php', + + /* The username and password which should be used for logging in. ('simplesaml' login type) */ + 'username' => 'username', + 'password' => 'secretpassword', + + /* The type of login page we expect. */ + 'logintype' => 'simplesaml', + + /* Expected attributes in the result. */ + 'attributes' => array( + 'uid' => 'test', + ), + ); + + +/* Add a test towards the specified IdP using the FEIDE login handler. */ +$tests[] = array( + + /* The full url to the admin/test.php page on the SP. */ + 'url' => 'https://example.org/simplesaml/admin/test.php', + + /* The idp we should test. */ + 'idp' => 'max.feide.no', + + /* The username, password and organization which should be used for logging in. ('feide' login type) */ + 'username' => 'username', + 'password' => 'secretpassword', + 'organization' => 'feide.no', + + /* The type of login page we expect. */ + 'logintype' => 'feide', + + /* Expected attributes in the result. */ + 'attributes' => array( + 'eduPersonAffiliation' => array( + 'employee', + 'staff', + 'student', + ), + ), + ); + -- GitLab