web/diag/radius_tests.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?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
 */

require_once dirname(dirname(dirname(__FILE__)))."/config/_config.php";

$loggerInstance = new \core\common\Logging();
$validator = new \web\lib\common\InputValidation();
$languageInstance = new \core\common\Language();
$languageInstance->setTextDomain("diagnostics");
$jsonDir = dirname(dirname(dirname(__FILE__)))."/var/json_cache";

$additional_message = [
    \core\common\Entity::L_OK => '',
    \core\common\Entity::L_REMARK => _("Some properties of the connection attempt were sub-optimal; the list is below."),
    \core\common\Entity::L_WARN => _("Some properties of the connection attempt were sub-optimal; the list is below."),
    \core\common\Entity::L_ERROR => _("Some configuration errors were observed; the list is below."),
];

/**
 * returns the friendly name of an EAP type
 * 
 * @param array $eap array representation of the EAP type to be returned
 * @return string the friendly name
 */
function disp_name($eap)
{
    $displayName = \core\common\EAP::eapDisplayName($eap);
    return $displayName['OUTER'].($displayName['INNER'] != '' ? '-'.$displayName['INNER'] : '');
}

if (!isset($_REQUEST['test_type']) || !$_REQUEST['test_type']) {
    throw new Exception("No test type specified!");
}

const VALID_TEST_TYPES = ['udp_login', 'udp', 'capath', 'clients', 'openroamingcapath', 'openroamingclients'];

$test_type = 'INVALID'; // will throw Exception if not replaced with correct
foreach (VALID_TEST_TYPES as $index => $oneType) {
    if ($_REQUEST['test_type'] == $oneType) {
        $test_type = VALID_TEST_TYPES[$index]; // from constant -> definitely not user-tainted
    }
}

$check_realm = $validator->realm($_REQUEST['realm']);
// $test_type: udp / capath / clients
if ($check_realm === FALSE) {
    throw new Exception("Invalid realm was submitted!");
}

if (isset($_REQUEST['profile_id'])) {
    $my_profile = $validator->existingProfile($_REQUEST['profile_id']);
    if (!$my_profile instanceof \core\ProfileRADIUS) {
        throw new Exception("RADIUS Tests can only be performed on RADIUS Profiles (d'oh!)");
    }
    $testsuite = new \core\diag\RADIUSTests($check_realm, $my_profile->getRealmCheckOuterUsername(), $my_profile->getEapMethodsinOrderOfPreference(1), $my_profile->getCollapsedAttributes()['eap:server_name'], $my_profile->getCollapsedAttributes()['eap:ca_file']);
} else {
    $my_profile = NULL;
    if (isset($_REQUEST['outer_user'])) {
        $testsuite = new \core\diag\RADIUSTests($check_realm, $_REQUEST['outer_user'].'@'.$check_realm);
    } else {
        $testsuite = new \core\diag\RADIUSTests($check_realm, '@'.$check_realm);
    }
}
session_write_close();

$hostindex = $_REQUEST['hostindex'];
if (!is_numeric($hostindex)) {
    throw new Exception("The requested host index is not numeric!");    
}

$token = '';
if (isset($_REQUEST['token'])) {
    $token = htmlspecialchars(strip_tags(filter_input(INPUT_GET, 'token') ?? filter_input(INPUT_POST, 'token')));
}

$ssltest = -1;
if (isset($_REQUEST['ssltest'])) {
    $ssltest = filter_input(INPUT_GET, 'ssltest', FILTER_SANITIZE_NUMBER_INT) ?? filter_input(INPUT_POST, 'ssltest', FILTER_SANITIZE_NUMBER_INT);
}

$posted_host = $_REQUEST['src'];
if (is_numeric($posted_host)) { // UDP tests, this is an index to the test host in config
    $host = filter_var(\config\Diagnostics::RADIUSTESTS['UDP-hosts'][$hostindex]['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    $expectedName = "IRRELEVANT-UDP";
} else { // dynamic discovery host, potentially unvetted user input
    // contains port number; needs to be redacted for filter_var to work
    // in any case, it's a printable string, so filter it initially
    $filteredHost = htmlspecialchars(strip_tags(filter_input(INPUT_GET, 'src') ?? filter_input(INPUT_POST, 'src')));
    $hostonly1 = preg_replace('/:[0-9]*$/', "", $filteredHost);
    $hostonly2 = preg_replace('/^\[/', "", $hostonly1);
    $hostonly3 = preg_replace('/\]$/', "", $hostonly2);
    $hostonly = filter_var($hostonly3, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    // check if this is a valid IP address
    //if ($hostonly === FALSE) {
    //    throw new Exception("The configured test host is not a valid IP address from acceptable IP ranges!");
    //}
    // host IP address testing passed. So let's take our port number back
    $host = $filteredHost;
    $expectedName = htmlspecialchars(strip_tags(filter_input(INPUT_GET, 'expectedname') ?? filter_input(INPUT_POST, 'expectedname')));
}
if (is_null($expectedName)) {
    $expectedName = '';
}
$returnarray = [];
$timeout = \config\Diagnostics::RADIUSTESTS['UDP-hosts'][$hostindex]['timeout'];
$consortiumName = 'eduroam';
switch ($test_type) {
    case 'udp_login':
        $i = 0;
        $returnarray['hostindex'] = $hostindex;
        $eaps = $my_profile->getEapMethodsinOrderOfPreference(1);
        $user_name = $validator->syntaxConformUser(isset($_REQUEST['username']) && $_REQUEST['username'] ? $_REQUEST['username'] : "");
        $outer_user_name = $validator->syntaxConformUser(isset($_REQUEST['outer_username']) && $_REQUEST['outer_username'] ? $_REQUEST['outer_username'] : $user_name);
        $testsuite->setOuterIdentity($outer_user_name);
        $user_password = isset($_REQUEST['password']) && $_REQUEST['password'] ? $_REQUEST['password'] : ""; //!!
        $returnarray['result'] = [];
        foreach ($eaps as $eap) {
            if ($eap->getIntegerRep() == \core\common\EAP::INTEGER_TLS) {
                $run_test = TRUE;
                if ($_FILES['cert']['error'] == UPLOAD_ERR_OK) {
                    $clientcertdata = file_get_contents($_FILES['cert']['tmp_name']);
                    $privkey_pass = isset($_REQUEST['privkey_pass']) && $_REQUEST['privkey_pass'] ? $_REQUEST['privkey_pass'] : ""; //!!
                    if (isset($_REQUEST['tls_username']) && $_REQUEST['tls_username']) {
                        $tls_username = $validator->syntaxConformUser(htmlspecialchars(strip_tags(filter_input(INPUT_POST, 'tls_username'))));
                    } else {
                        if (openssl_pkcs12_read($clientcertdata, $certs, $privkey_pass)) {
                            $mydetails = openssl_x509_parse($certs['cert']);
                            if (isset($mydetails['subject']['CN']) && $mydetails['subject']['CN']) {
                                $tls_username = $mydetails['subject']['CN'];
                                $loggerInstance->debug(4, "PKCS12-CN=$tls_username\n");
                            } else {
                                $testresult = \core\diag\RADIUSTests::RETVAL_INCOMPLETE_DATA;
                                $run_test = FALSE;
                            }
                        } else {
                            $testresult = \core\diag\RADIUSTests::RETVAL_WRONG_PKCS12_PASSWORD;
                            $run_test = FALSE;
                        }
                    }
                } else {
                    $testresult = \core\diag\RADIUSTests::RETVAL_INCOMPLETE_DATA;
                    $run_test = FALSE;
                }
                if ($run_test) {
                    $loggerInstance->debug(4, "TLS-USERNAME=$tls_username\n");
                    $testresult = $testsuite->udpLogin($hostindex, $eap->getArrayRep(), $tls_username, $privkey_pass, TRUE, TRUE, $clientcertdata);
                }
            } else {
                $testresult = $testsuite->udpLogin($hostindex, $eap->getArrayRep(), $user_name, $user_password);
            }
            $returnarray['result'][$i] = $testsuite->consolidateUdpResult($hostindex);
            $returnarray['result'][$i]['eap'] = $eap->getPrintableRep();
            $returnarray['returncode'][$i] = $testresult;


            switch ($testresult) {
                case \core\diag\RADIUSTests::RETVAL_OK:
                    $level = $returnarray['result'][$i]['level'];
                    switch ($level) {
                        case \core\common\Entity::L_OK:
                            $message = _("<strong>Test successful.</strong>");
                            break;
                        case \core\common\Entity::L_REMARK:
                        case \core\common\Entity::L_WARN:
                            $message = _("<strong>Test partially successful</strong>: authentication succeeded.").' '.$additional_message[$level];
                            break;
                        case \core\common\Entity::L_ERROR:
                            $message = _("<strong>Test FAILED</strong>: authentication succeeded.").' '.$additional_message[$level];
                            break;
                    }
                    break;
                case \core\diag\RADIUSTests::RETVAL_CONVERSATION_REJECT:
                    $message = _("<strong>Test FAILED</strong>: the request was rejected. The most likely cause is that you have misspelt the Username and/or the Password.");
                    $level = \core\common\Entity::L_ERROR;
                    break;
                case \core\diag\RADIUSTests::RETVAL_NOTCONFIGURED:
                    $level = \core\common\Entity::L_ERROR;
                    $message = _("This method cannot be tested");
                    break;
                case \core\diag\RADIUSTests::RETVAL_IMMEDIATE_REJECT:
                    $level = \core\common\Entity::L_ERROR;
                    $message = _("<strong>Test FAILED</strong>: the request was rejected immediately, without EAP conversation. Either you have misspelt the Username or there is something seriously wrong with your server.");
                    unset($returnarray['result'][$i]['cert_oddities']);
                    $returnarray['result'][$i]['server'] = 0;
                    break;
                case \core\diag\RADIUSTests::RETVAL_NO_RESPONSE:
                    $level = \core\common\Entity::L_ERROR;
                    $message = sprintf(_("<strong>Test FAILED</strong>: no reply from the RADIUS server after %d seconds. Either the responsible server is down, or routing is broken!"), $timeout);
                    unset($returnarray['result'][$i]['cert_oddities']);
                    $returnarray['result'][$i]['server'] = 0;
                    break;
                case \core\diag\RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM:
                    $returnarray['message'] = sprintf(_("<strong>Test FAILED</strong>: there was a bidirectional RADIUS conversation, but it did not finish after %d seconds!"), $timeout);
                    $returnarray['level'] = \core\common\Entity::L_ERROR;
                    break;
                default:
                    $level = isset($testsuite->returnCodes[$testresult]['severity']) ? $testsuite->returnCodes[$testresult]['severity'] : \core\common\Entity::L_ERROR;
                    $message = isset($testsuite->returnCodes[$testresult]['message']) ? $testsuite->returnCodes[$testresult]['message'] : _("<strong>Test FAILED</strong>");
                    $returnarray['result'][$i]['server'] = 0;
                    break;
            }
            $returnarray['result'][$i]['level'] = $level;
            $returnarray['result'][$i]['message'] = $message;
            $i++;
        }
        break;
    case 'udp':
        $i = 0;
        $returnarray['hostindex'] = $hostindex;
        $testresult = $testsuite->udpReachability($hostindex);
        $returnarray['result'][$i] = $testsuite->consolidateUdpResult($hostindex);
        $returnarray['result'][$i]['eap'] = 'ALL';
        $returnarray['returncode'][$i] = $testresult;
        // a failed check may not have got any certificate, be prepared for that
        switch ($testresult) {
            case \core\diag\RADIUSTests::RETVAL_CONVERSATION_REJECT:
                $level = $returnarray['result'][$i]['level'];
                if ($level > \core\common\Entity::L_OK) {
                    $message = _("<strong>Test partially successful</strong>: a bidirectional RADIUS conversation with multiple round-trips was carried out, and ended in an Access-Reject as planned.").' '.$additional_message[$level];
                } else {
                    $message = _("<strong>Test successful</strong>: a bidirectional RADIUS conversation with multiple round-trips was carried out, and ended in an Access-Reject as planned.");
                }
                break;
            case \core\diag\RADIUSTests::RETVAL_IMMEDIATE_REJECT:
                $message = _("<strong>Test FAILED</strong>: the request was rejected immediately, without EAP conversation. This is not necessarily an error: if the RADIUS server enforces that outer identities correspond to an existing username, then this result is expected (Note: you could configure a valid outer identity in your profile settings to get past this hurdle). In all other cases, the server appears misconfigured or it is unreachable.");
                $level = \core\common\Entity::L_WARN;
                break;
            case \core\diag\RADIUSTests::RETVAL_NO_RESPONSE:
                $returnarray['result'][$i]['server'] = 0;
                $message = sprintf(_("<strong>Test FAILED</strong>: no reply from the RADIUS server after %d seconds. Either the responsible server is down, or routing is broken!"), $timeout);
                $level = \core\common\Entity::L_ERROR;
                break;
            case \core\diag\RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM:
                $message = sprintf(_("<strong>Test FAILED</strong>: there was a bidirectional RADIUS conversation, but it did not finish after %d seconds!"), $timeout);
                $level = \core\common\Entity::L_ERROR;
                break;
            default:
                $message = _("unhandled error");
                $level = \core\common\Entity::L_ERROR;
                break;
        }
        $loggerInstance->debug(4, "SERVER=".$returnarray['result'][$i]['server']."\n");
        $returnarray['result'][$i]['level'] = $level;
        $returnarray['result'][$i]['message'] = $message;
        break;
    case 'openroamingcapath':
        $consortiumName = 'openroaming';
    case 'capath':
        $rfc6614suite = new \core\diag\RFC6614Tests([$host], $expectedName, $consortiumName);
        $returnarray['IP'] = $host;
        $returnarray['hostindex'] = $hostindex;
        $returnarray['consortium'] = $consortiumName;
        $returnarray['name'] = $expectedName;
        if ($ssltest) {
            $testresult = $rfc6614suite->cApathCheck($host);
            if ($testresult == \core\diag\RADIUSTests::RETVAL_INVALID) {
                $returnarray['result'] = $testresult;
                $returnarray['level'] = \core\common\Entity::L_ERROR;
                break;
            }
        } else {
            $testresult = \core\diag\RADIUSTests::RETVAL_SKIPPED;
            $returnarray['message'] = _("<strong>ERROR</strong>: connectivity problem!");
            $returnarray['level'] = \core\common\Entity::L_WARN;
        }
        
        // the host member of the array may not be set if RETVAL_SKIPPED was
        // returned (e.g. IPv6 host), be prepared for that
        if (!isset($rfc6614suite->TLS_CA_checks_result[$host])) {
            $returnarray['result'] = $testresult;
            break;
        }
        // we tried to contact someone, and know how long that took
        $returnarray['time_millisec'] = sprintf("%d", $rfc6614suite->TLS_CA_checks_result[$host]['time_millisec']);
        $timeDisplay = ' ('.sprintf(_("elapsed time: %d"), $rfc6614suite->TLS_CA_checks_result[$host]['time_millisec']).'&nbsp;ms)';
        if (isset($rfc6614suite->TLS_CA_checks_result[$host]['cert_oddity']) && ($rfc6614suite->TLS_CA_checks_result[$host]['cert_oddity'] == \core\diag\RADIUSTests::CERTPROB_UNKNOWN_CA)) {
            $returnarray['message'] = _("<strong>ERROR</strong>: the server presented a certificate which is from an unknown authority!").$timeDisplay;
            $returnarray['level'] = \core\common\Entity::L_ERROR;
            $returnarray['result'] = $testresult;
            break;
        }
        // probably all is well, but we override this default later if need be
        $returnarray['message'] = $rfc6614suite->returnCodes[$rfc6614suite->TLS_CA_checks_result[$host]['status']]["message"];
        $returnarray['level'] = \core\common\Entity::L_OK;
        // override if the connection was with a mismatching server name
        if (isset($rfc6614suite->TLS_CA_checks_result[$host]['cert_oddity']) && ($rfc6614suite->TLS_CA_checks_result[$host]['cert_oddity'] == \core\diag\RADIUSTests::CERTPROB_DYN_SERVER_NAME_MISMATCH)) {
            $returnarray['message'] = _("<strong>WARNING</strong>: the server name as discovered in the SRV record does not match any name in the server certificate!").$timeDisplay;
            $returnarray['level'] = \core\common\Entity::L_WARN;
        }
        switch ($rfc6614suite->TLS_CA_checks_result[$host]['status']) {
            case \core\diag\RADIUSTests::RETVAL_CONNECTION_REFUSED:
                $returnarray['level'] = \core\common\Entity::L_ERROR;
                break;
            case \core\diag\RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM:
                $returnarray['level'] = \core\common\Entity::L_ERROR;
                break;
            case \core\diag\RADIUSTests::RETVAL_OK:
            $returnarray['certdata'] = [];
            $returnarray['certdata']['subject'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['subject'];
            $returnarray['certdata']['issuer'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['issuer'];
            $returnarray['certdata']['validTo'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['validTo'];
            $returnarray['certdata']['extensions'] = [];
            if (isset($rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'])) {
                $returnarray['certdata']['extensions']['subjectaltname'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'];
            }
            if (isset($rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['policyoid'])) {
                $returnarray['certdata']['extensions']['policies'] = join(' ', $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['policyoid']);
            }
            if (isset($rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['crlDistributionPoint'])) {
                $returnarray['certdata']['extensions']['crldistributionpoints'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['crlDistributionPoint'];
            }
            if (isset($rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['authorityInfoAccess'])) {
                $returnarray['certdata']['extensions']['authorityinfoaccess'] = $rfc6614suite->TLS_CA_checks_result[$host]['certdata']['extensions']['authorityInfoAccess'];
            }
            break;
            default:
                $returnarray['message'] .= $timeDisplay;
        }
        
        $returnarray['cert_oddities'] = [];
        $returnarray['result'] = $testresult;
        break;
    case 'openroamingclient':
        $consortiumName = 'openroaming';
    case 'clients':
        $rfc6614suite = new \core\diag\RFC6614Tests([$host], $expectedName, $consortiumName);
        if ($ssltest) {
            $testresult = $rfc6614suite->tlsClientSideCheck($host, $expectedName, $check_realm);
        } else {
            $testresult = \core\diag\RADIUSTests::RETVAL_SKIPPED;
            $returnarray['message'] = _("<strong>ERROR</strong>: connectivity problem!");
            $returnarray['level'] = \core\common\Entity::L_WARN;
        }
        $returnarray['IP'] = $host;
        $returnarray['hostindex'] = $hostindex;
        $returnarray['name'] = $expectedName;
        $returnarray['consortium'] = $consortiumName;
        $k = 0;
        // the host member of the array may not exist if RETVAL_SKIPPED came out
        // (e.g. no client cert to test with). Be prepared for that
        if (isset($rfc6614suite->TLS_clients_checks_result[$host])) {
            foreach ($rfc6614suite->TLS_clients_checks_result[$host]['ca'] as $type => $cli) {
                foreach ($cli as $key => $val) {
                    $returnarray['ca'][$k][$key] = $val;
                }
                $k++;
            }
        }
        $returnarray['result'] = $testresult;
        break;
    default:
        throw new Exception("Unknown test requested: default case reached!");
}
$returnarray['datetime'] = date("Y-m-d H:i:s");
if ($token!= '' && is_dir($jsonDir.'/'.$token)) {
    @mkdir($jsonDir.'/'.$token, 0777, true);
}
$json_data = json_encode($returnarray);
if ($token != '') {
    file_put_contents($jsonDir.'/'.$token.'/'.$test_type.'_'.$hostindex, $json_data);
}
header("Content-type: application/json; utf-8");
echo($json_data);