core/diag/RADIUSTestsUI.php
<?php
/*
* *****************************************************************************
* Contributions to this work were made on behalf of the GÉANT project, a
* project that has received funding from the European Union’s Framework
* Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
* Horizon 2020 research and innovation programme under Grant Agreements No.
* 691567 (GN4-1) and No. 731122 (GN4-2).
* On behalf of the aforementioned projects, GEANT Association is the sole owner
* of the copyright in all material which was developed by a member of the GÉANT
* project. GÉANT Vereniging (Association) is registered with the Chamber of
* Commerce in Amsterdam with registration number 40535155 and operates in the
* UK as a branch of GÉANT Vereniging.
*
* Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands.
* UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
*
* License: see the web/copyright.inc.php file in the file structure or
* <base_url>/copyright.php after deploying the software
*/
/**
* This file contains code for testing presenting tests result
*
* @author Maja Gorecka-Wolniewicz <mgw@umk.pl>
*
* @package Developer
*
*/
namespace core\diag;
use \Exception;
class RADIUSTestsUI extends AbstractTest
{
/**
* This private variable contains the realm to be checked. Is filled in the
* class constructor.
*
* @var string
*/
public $realm = NULL;
public $outerUser = NULL;
/**
* result of the reachability tests
*
* @var array
*/
public $allReachabilityResults = [];
private $hostMap = [];
private $protocolsMap = [];
private $globalLevelStatic = \core\common\Entity::L_OK;
private $globalLevelDynamic = \core\common\Entity::L_OK;
private $rfc7585suite = NULL;
private $srv;
private $naptr;
private $naptrValid;
private $hosts;
private $testSuite;
private $areFailed = FALSE;
private $allCApathFailed = FALSE;
private $globalInfo = [];
private $stateIcons = [];
private $states;
private $certFields;
private $timestamp;
const RADIUS_TEST_OPERATION_MODE_SHALLOW = 1;
const RADIUS_TEST_OPERATION_MODE_THOROUGH = 2;
/**
* Constructor for the RADIUSTestsUI class. The single mandatory parameter is the
* token indicating tests that were carried out and saved as JSON files.
*
* @param string $token the token which points to a directory
* @throws Exception
*/
public function __construct($token)
{
parent::__construct();
$this->globalInfo = [
\core\common\Entity::L_OK => _("All tests passed."),
\core\common\Entity::L_WARN => _("There were some warnings."),
\core\common\Entity::L_ERROR => _("There were some errors."),
\core\common\Entity::L_REMARK => _("There were some remarks.")
];
$this->stateIcons = [
\core\common\Entity::L_OK => '../resources/images/icons/Tabler/square-rounded-check-filled-green.svg',
\core\common\Entity::L_WARN => '../resources/images/icons/Tabler/alert-square-rounded-filled-yellow.svg',
\core\common\Entity::L_ERROR => '../resources/images/icons/Tabler/square-rounded-x-filled-red.svg',
\core\common\Entity::L_REMARK => '../resources/images/icons/Tabler/info-square-rounded-filled-blue.svg'
];
$this->states = [
'PASS' => _("PASS"),
'FAIL' => _("FAIL")
];
$this->certFields = [
'subject' => _("Subject:"),
'issuer' => _("Issuer:"),
'validFrom' => _("Valid from:"),
'validTo' => _("Valid to:"),
'serialNumber' => _("Serial number:"),
'sha1' => _("SHA1 fingerprint:"),
'public_key_length' => _("Public key length:"),
'title' => _("Server certificate"),
'policies' => _("Policies:"),
'crldistributionpoints' => _("crlDistributionPoint:"),
'authorityinfoaccess' => _("authorityInfoAccess:"),
'subjectaltname' => _("SubjectAltName:"),
];
$jsondir = dirname(dirname(dirname(__FILE__)))."/var/json_cache";
if ($token && is_dir($jsondir.'/'.$token)) {
foreach (['realm', 'udp', 'clients', 'capath'] as $test_type) {
foreach (glob("$jsondir/$token/$test_type*") as $filename) {
$this->loggerInstance->debug(4, "\nIS_DIR $filename\n");
if (!array_key_exists($test_type, $this->allReachabilityResults)) {
$this->allReachabilityResults[$test_type] = array();
}
$this->allReachabilityResults[$test_type][] = json_decode(file_get_contents($filename));
}
}
if ($this->allReachabilityResults['realm'][0]->realm) {
$this->realm = $this->allReachabilityResults['realm'][0]->realm;
$this->outerUser = $this->allReachabilityResults['realm'][0]->outeruser;
foreach ($this->allReachabilityResults['realm'][0]->totest as $totest) {
$this->hostMap[$totest->host] = $totest->bracketaddr;
if (property_exists($totest, 'protocols')) {
$this->protocolsMap[$totest->host] = $totest->protocols;
}
}
$this->rfc7585suite = unserialize(base64_decode($this->allReachabilityResults['realm'][0]->rfc7585suite));
$this->srv = $this->allReachabilityResults['realm'][0]->srv;
$this->naptr = $this->allReachabilityResults['realm'][0]->naptr;
$this->naptrValid = $this->allReachabilityResults['realm'][0]->naptr_valid;
$this->hosts = $this->allReachabilityResults['realm'][0]->hosts;
$this->testSuite = unserialize(base64_decode($this->allReachabilityResults['realm'][0]->testsuite));
}
$this->timestamp = $this->allReachabilityResults['realm'][0]->datetime;
}
}
public function getTimeStamp()
{
return $this->timestamp;
}
/**
* sets the global status for static tests
*/
public function setGlobalStaticResult()
{
foreach ($this->allReachabilityResults['udp'] as $udp) {
$this->globalLevelStatic = max($this->globalLevelStatic, $udp->result[0]->level);
}
}
public function setGlobalDynamicResult()
{
if (isset($this->allReachabilityResults['capath'])) {
foreach ($this->allReachabilityResults['capath'] as $capath) {
$this->globalLevelDynamic = max($this->globalLevelDynamic, $capath->level);
}
}
if (isset($this->allReachabilityResults['clients'])) {
foreach ($this->allReachabilityResults['clients'] as $clients) {
$srefused = FALSE;
$level = \core\common\Entity::L_OK;
$maxlevel = $level;
foreach ($clients->ca as $ca) {
foreach ($ca->certificate as $certificate) {
if ($certificate->returncode == \core\diag\RADIUSTests::RETVAL_CONNECTION_REFUSED) {
$srefused = $this->areFailed = TRUE;
}
}
if (!$srefused) {
foreach ($clients->ca as $cca) {
foreach ($cca->certificate as $certificate) {
$level = $certificate->returncode;
if ($level < 0) {
$level = \core\common\Entity::L_ERROR;
$this->areFailed = TRUE;
}
if ($certificate->expected != 'PASS') {
if ($certificate->connected == 1) {
$level = \core\common\Entity::L_WARN;
} else {
if ($certificate->status == 'EXPIRED' && property_exists($certificate, 'reason')) {
$level = \core\common\Entity::L_WARN;
} else {
$level = \core\common\Entity::L_OK;
}
}
}
if ($level > $maxlevel) {
$maxlevel = $level;
}
}
}
}
}
$this->globalLevelDynamic = max($this->globalLevelDynamic, $maxlevel);
}
}
}
public function isDynamic()
{
if ($this->naptr > 0) {
return TRUE;
}
return FALSE;
}
/**
* prints tabs-1
*
*
*/
public function printOverview()
{
$out = [];
$out[] = "<fieldset class='option_container'>
<legend>
<strong>"._("Overview").'</strong>
</legend>';
$out[] = "<strong>"._("DNS checks")."</strong><div>";
if ($this->naptr != \core\diag\RADIUSTests::RETVAL_NOTCONFIGURED) {
$out[] = "<table>";
$out[] = "<tr><td>"._("Checking NAPTR existence:")."</td><td>";
switch ($this->naptr) {
case \core\diag\RFC7585Tests::RETVAL_NONAPTR:
$out[] = _("This realm has no NAPTR records.");
break;
case \core\diag\RFC7585Tests::RETVAL_ONLYUNRELATEDNAPTR:
$out[] = _("This realm has NAPTR records, but none are related to this roaming consortium.");
break;
default: // if none of the possible negative retvals, then we have matching NAPTRs
$out[] = sprintf(_("This realm has %d NAPTR records relating to this roaming consortium."), $this->naptr);
}
$out[] = "</td></tr>";
if ($this->naptr > 0) {
$out[] = "<tr><td>"._("Checking NAPTR compliance (flag = S and regex = {empty}):")."</td><td>";
switch ($this->naptrValid) {
case \core\diag\RADIUSTests::RETVAL_OK:
$out[] = "No issues found.";
break;
case \core\diag\RADIUSTests::RETVAL_INVALID:
$out[] = _("At least one NAPTR with invalid content found!");
break;
}
$out[] = "</td></tr>";
}
// SRV resolution
if ($this->naptr > 0 && $this->naptrValid == \core\diag\RADIUSTests::RETVAL_OK) {
$out[] = "<tr><td>"._("Checking SRVs:")."</td><td>";
switch ($this->srv) {
case \core\diag\RADIUSTests::RETVAL_SKIPPED:
$out[] = _("This check was skipped.");
break;
case \core\diag\RADIUSTests::RETVAL_INVALID:
$out[] = _("At least one NAPTR with invalid content found!");
break;
default: // print number of successfully retrieved SRV targets
$out[] = sprintf(_("%d host names discovered."), $this->srv);
}
$out[] = "</td></tr>";
}
// IP addresses for the hosts
if ($this->naptr > 0 && $this->naptrValid == \core\diag\RADIUSTests::RETVAL_OK && $this->srv > 0) {
$out[] = "<tr><td>"._("Checking IP address resolution:")."</td><td>";
switch ($this->srv) {
case \core\diag\RADIUSTests::RETVAL_SKIPPED:
$out[] = _("This check was skipped.");
break;
case \core\diag\RADIUSTests::RETVAL_INVALID:
$out[] = _("At least one hostname could not be resolved!");
break;
default: // print number of successfully retrieved SRV targets
$out[] = sprintf(_("%d IP addresses resolved."), $this->hosts);
}
$out[] = "</td></tr>";
}
$out[] = "</table><br/>";
$out[] = sprintf(_("Realm is <strong>%s</strong> "), _(($this->naptr > 0 ? "DYNAMIC" : "STATIC")));
if (count($this->testSuite->listerrors()) == 0) {
$out[] = _("with no DNS errors encountered. Congratulations!");
} else {
$out[] = _("but there were DNS errors! Check them!")." "._("You should re-run the tests after fixing the errors; more errors might be uncovered at that point. The exact error causes are listed below.");
$out[] = "<div class='notacceptable'><table>";
foreach ($this->testSuite->listerrors() as $details) {
$out[] = "<tr><td>".$details['TYPE']."</td><td>".$details['TARGET']."</td></tr>";
}
$out[] = "</table></div>";
}
$out[] = '</div>';
} else {
$out[] = "<tr><td>"._("Dynamic discovery test is not configured")."</td><td>";
}
$out[] = "<hr><strong>"._("Static connectivity tests")."</strong>
<table><tr>
<td class='icon_td'>";
$out[] = "<img src='".$this->stateIcons[$this->globalLevelStatic]."' id='main_static_ico' class='icon'></td><td id='main_static_result'>".
$this->globalInfo[$this->globalLevelStatic].' '. _("See the appropriate tab for details.").'</td>
</tr></table>';
if ($this->naptr > 0) {
$out[] = "<hr><strong>"._("Dynamic connectivity tests")."</strong>
<table><tr>
<td class='icon_td'><img src='".$this->stateIcons[$this->globalLevelDynamic]."' id='main_dynamic_ico' class='icon'></td><td id='main_dynamic_result'>".
$this->globalInfo[$this->globalLevelDynamic].' '._("See the appropriate tab for details.").'</td></tr></table>';
}
$out[] = '</fieldset>';
return join('', $out);
}
public function printStatic()
{
$out = [];
$out[] = '<fieldset class="option_container" id="static_tests">
<legend><strong>';
$out[] = _("STATIC connectivity tests");
$out[] = '</strong> </legend>';
$out[] = _("This check sends a request for the realm through various entry points of the roaming consortium infrastructure. The request will contain the 'Operator-Name' attribute, and will be larger than 1500 Bytes to catch two common configuration problems.<br/>Since we don't have actual credentials for the realm, we can't authenticate successfully - so the expected outcome is to get an Access-Reject after having gone through an EAP conversation.");
$out[] = '<p>';
foreach ($this->allReachabilityResults['udp'] as $udp) {
$hostindex = $udp->hostindex;
$result = $udp->result[0];
$out[] = '<hr>';
$out[] = sprintf(_("Testing from: <strong>%s"), \config\Diagnostics::RADIUSTESTS['UDP-hosts'][$hostindex]['display_name']).'</strong>';
$out[] = '<ul style="list-style-type: none;"><li>';
$out[] = "<table id='results$hostindex' style='width:100%' class='udp_results'>
<tr>
<td class='icon_td'><img src='".$this->stateIcons[$result->level]."' id='src".$hostindex."_img'></td>
<td id='src$hostindex' colspan=2>
";
$out[] = '<strong>'.($result->server ? $result->server : _("Connected to undetermined server")).'</strong><br/>'.sprintf (_("elapsed time: %sms."), $result->time_millisec).'<div>'.$result->message.'</div>';
if ($result->level > \core\common\Entity::L_OK && property_exists($result, 'cert_oddities')) {
foreach ($result->cert_oddities as $oddities) {
$out[] = '<tr class="results_tr"><td> </td><td class="icon_td"><img src="'.$this->stateIcons[$oddities->level].'"></td><td>'.$oddities->message.'</td></tr>';
}
}
$more = '';
if ($result->server_cert) {
$more .= '<div class="more">';
$certdesc = '<br>'.$this->certFields['title'].'<ul>';
foreach ($result->server_cert as $sckey => $sc) {
if (array_key_exists($sckey, $this->certFields)) {
$certdesc .= '<li>'.$this->certFields[$sckey].' '.$sc;
}
}
if ($result->server_cert->extensions) {
$certdesc .= '<li>' . _('Extensions') . '<ul>';
foreach ($result->server_cert->extensions as $ekey => $eval) {
$certdesc .= '<li>' . $ekey . ': ' . $eval;
}
$certdesc .= '</ul>';
}
$certdesc .= '</ul>';
$more .= '<span class="morecontent"><span>'.$certdesc.
'</span><a href="" class="morelink">'._("show server certificate details").'»</a></span></div>';
}
if ($more != '' ) {
$out[] = '<tr><td> </td><td colspan="2">'.$more.'</td></tr>';
}
$out[] = "</table></ul>";
}
$out[] = '</fieldset>';
return join('', $out);
}
private function collectCAPath()
{
$capathtest = [];
$capathtest[] = '<p><strong>'._("Checking server handshake...")."</strong><p>";
$allfailed = TRUE;
foreach ($this->allReachabilityResults['capath'] as $capath) {
$hostindex = $capath->hostindex;
if ($capath->level == \core\common\Entity::L_OK && $capath->result == \core\diag\RADIUSTests::RETVAL_INVALID) {
$capath->level = \core\common\Entity::L_ERROR;
}
$level = $capath->level;
if ($level == \core\common\Entity::L_OK) {
$allfailed = FALSE;
}
$capathtest[] = '<p><strong>'.$this->hostMap[$capath->IP].'</strong> ('.$capath->name.') ';
$prots = [];
if (isset($this->protocolsMap[$capath->IP]) && $this->protocolsMap[$capath->IP] != '') {
$prots = explode(';', $this->protocolsMap[$capath->IP]);
if (!empty($prots)) {
$capathtest[] = ' ' . _("supported TLS protocols: ");
$capathtest[] = implode(', ', $prots);
if (!in_array("TLS1.3", $prots)) {
$capathtest[] = ' ' . '<font color="red">' . _("not supported: ") . 'TLS1.3</font>';
}
}
}
$capathtest[] = '<ul style="list-style-type: none;" class="caresult"><li>';
$capathtest[] = "<table id='caresults$hostindex' style='width:100%'>
<tr>
<td class='icon_td'><img src='";
$capathtest[] = $this->stateIcons[$level]."' id='srcca".$hostindex."_img'></td>
<td id='srcca$hostindex'>";
$more = '';
if ($capath->certdata && $capath->certdata->subject != '') {
$more .= '<div class="more">';
$certdesc = '<br>'.$this->certFields['title'].'<ul>';
if ($capath->certdata->subject) {
$certdesc .= '<li>'.$this->certFields['subject'].' '.$capath->certdata->subject;
}
if ($capath->certdata->issuer) {
$certdesc .= '<li>'.$this->certFields['issuer'].' '.$capath->certdata->issuer;
}
if ($capath->certdata->validTo) {
$certdesc .= '<li>'.$this->certFields['validTo'].' '.
date_create_from_format('ymdGis',
substr($capath->certdata->validTo, 0, -1))->format('Y-m-d H:i:s'). ' UTC';
}
if ($capath->certdata->extensions) {
if ($capath->certdata->extensions->subjectaltname) {
$certdesc .= '<li>'.$this->certFields['subjectaltname'].' '.$capath->certdata->extensions->subjectaltname;
}
}
if ($capath->certdata->extensions->policies) {
$certdesc .= '<li>'.$this->certFields['policies'].' '.$capath->certdata->extensions->policies;
}
if ($capath->certdata->extensions->crldistributionpoints) {
$certdesc .= '<li>'.$this->certFields['crldistributionpoints'].' '.$capath->certdata->extensions->crldistributionpoints;
}
if ($capath->certdata->extensions->authorityinfoaccess) {
$certdesc .= '<li>'.$this->certFields['authorityinfoaccess'].' '.$capath->certdata->extensions->authorityinfoaccess;
}
$certdesc .= '</ul>';
$more .= '<span class="morecontent"><span>'.$certdesc.$protocoldesc.
'</span> <a href="" class="morelink">'._("more").'»</a></span></td></tr>';
} else {
$certdesc = '<br>';
}
$capathtest[] = '<div>'.($capath->message!='' ? $capath->message : _('Test failed')).'</div>'.$more;
$capathtest[] = '</td>
</tr>
</table>';
$capathtest[] = '</li></ul>';
}
$this->allCApathFailed = $allfailed;
return $capathtest;
}
private function collectClients()
{
$clientstest = [];
foreach ($this->allReachabilityResults['clients'] as $clients) {
if ($clients->result == RADIUSTests::RETVAL_SKIPPED) {
continue;
}
$hostindex = $clients->hostindex;
$clientstest[] = '<p><strong>'.$this->hostMap[$clients->IP].'</strong></p>';
$clientstest[] = "<span id='clientresults$hostindex'>";
$clientstest[] = '<p></p>';
if ($this->globalLevelDynamic != \core\common\Entity::L_ERROR) {
if (property_exists($clients, 'ca')) {
$clientstest[] = '<ol>';
foreach ($clients->ca as $ca) {
$srefused = 0;
$cliinfo = '';
$cliinfo .= '<li>'._('Client certificate').' <b>'.$ca->clientcertinfo->from.
'</b>'.', '.$ca->clientcertinfo->message .
'<br> (CA: '.$ca->clientcertinfo->issuer.')<ul>';
foreach ($ca->certificate as $certificate) {
if ($certificate->returncode == \core\diag\RADIUSTests::RETVAL_CONNECTION_REFUSED) {
$srefused = 1;
}
}
if ($srefused == 0) {
foreach ($ca->certificate as $certificate) {
$cliinfo .= '<li><i>'.$certificate->message.
', '._("expected result: ").$this->states[$certificate->expected].'</i>';
$cliinfo .= '<ul style="list-style-type: none;">';
if (property_exists($certificate, 'finalerror') && $certificate->finalerror == 2) {
$cliinfo .= '<li>'._('this test was skipped - no appropriate client certificate').'</li></ul>';
continue;
}
$level = $certificate->returncode;
if ($level < 0) {
$level = \core\common\Entity::L_ERROR;
}
$add = '';
if ($certificate->expected == 'PASS') {
if ($certificate->connected == 1) {
$state = _("Server accepted this client certificate");
} else {
if (property_exists($certificate, 'reason') && $certificate->reason == \core\diag\RADIUSTests::CERTPROB_UNKNOWN_CA) {
$add = '<br>'._('You should update your list of accredited CAs').
' <a href=\"'.\config\Diagnostics::RADIUSTESTS['accreditedCAsURL'].'\">'.
_('Get it from here.').'</a>';
}
$state = _('Server did not accept this client certificate - reason').': '.
$certificate->resultcomment;
}
} else {
if ($certificate->connected == 1) {
$level = \core\common\Entity::L_WARN;
$state = _('Server accepted this client certificate, but should not have');
} else {
$level = \core\common\Entity::L_OK;
$state = _('Server did not accept this client certificate').': '.$certificate->resultcomment;
}
}
$cliinfo .= '<li><table><tbody><tr><td class="icon_td"><img class="icon" src="'.$this->stateIcons[$level].'" style="width: 24px;"></td><td>'.$state;
$cliinfo .= ' ('.sprintf(_('elapsed time: %sms.'), $certificate->time_millisec).' ) '.$add.'</td></tr>';
$cliinfo .= '</tbody></table></ul></li>';
if (property_exists($certificate, 'finalerror')) {
if ($certificate->finalerror == 1) {
$cliinfo .= '<li>'._('Rest of tests for this CA skipped').'</li>';
}
}
}
$cliinfo .= '</ul>';
}
if ($srefused > 0) {
$cliinfo = _('Connection refused');
$clientstest[] = "<table><tr><td class='icon_td' id='srcclient".$hostindex."_img'><img src='".$this->stateIcons[\core\common\Entity::L_ERROR]."'></td>".
"<td id='srcclient$hostindex'><p>$cliinfo</p></td></tr></table>";
} else {
$clientstest[] = "<p>$cliinfo</p>";
}
}
} else {
$cliinfo = _('Test failed');
$clientstest[] = "<table><tr><td class='icon_td' id='srcclient".$hostindex."_img'><img src='".
$this->stateIcons[\core\common\Entity::L_WARN]."'></td>" .
"<td id='srcclient$hostindex'>$cliinfo</td></tr></table>";
}
} else {
$clientstest[] = '<ul style="list-style-type: none;" class="clientsresult"><li>';
$clientstest[] = "<table id='clientsresults$hostindex' style='width:100%'>
<tr>
<td class='icon_td'><img src='";
$clientstest[] = $this->stateIcons[\core\common\Entity::L_ERROR]."' id='srcclients".$hostindex."_img'></td>
<td id='srcclient$hostindex'>";
$clientstest[] = _("These tests were skipped because of previous errors.").'</td></tr></table></ul>';
}
$clientstest[] = '</ol><p></p>';
}
return $clientstest;
}
public function printDynamic()
{
$out = [];
$out[] = "<div id='dynamic_tests'><fieldset class='option_container'>
<legend><strong>"._("DYNAMIC connectivity tests")."</strong></legend>";
if (count($this->rfc7585suite->NAPTR_hostname_records) > 0) {
$capathtest = $this->collectCAPath();
$clientstest = $this->collectClients();
$out[] = '<div style="align:right;">';
$out[] = '<div style="align:right; display: ';
if ($this->globalLevelDynamic == \core\common\Entity::L_OK && !$this->areFailed) {
$out[] = 'none';
}
$out[] = ';" id="dynamic_result_fail"><b>'._("Some errors were found during the tests, see below").'</b></div>';
$out[] = '<div style="align:right; display: ';
if ($this->globalLevelDynamic != \core\common\Entity::L_OK || $this->areFailed) {
$out[] = 'none';
}
$out[] = '" id="dynamic_result_pass"><b>'.
_("All tests passed, congratulations!").'</b></div>';
if (!$this->allCApathFailed) {
$out[] = '<div style="align:left;"><a href="" class="moreall"><i>'._('Show detailed information for all tests').'»</i></a></div>';
}
$out[] = join('', $capathtest);
if (!empty($clientstest)) {
$out[] = '<span id="clientstest" style="display: ;"><p><hr><b>'._('Checking if certificates from CAs are accepted...').'</b><p>'._('A few client certificates will be tested to check if servers are resistant to some certificate problems.').'<p>';
$out[] = join('', $clientstest);
$out[] = '</span>';
}
$out[] = '</div>';
}
$out[] = "</fieldset></div></div>";
return join('', $out);
}
}