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