diff --git a/page-logic/eccs.php b/page-logic/eccs.php new file mode 100644 index 0000000000000000000000000000000000000000..4ab2159c325450b9ead0a32a8c36b3746812b901 --- /dev/null +++ b/page-logic/eccs.php @@ -0,0 +1,25 @@ +<?php +$directory = "/srv/eccs/output"; +$files = scandir($directory); +$firstFile = $files[3]; // [0] = '.' ; [1] = '..' ; [2] = '.gitignore' + +$str2strip = array("eccs_", ".log"); +$firstDate = str_replace($str2strip, "", $firstFile); + +$files = scandir($directory, SCANDIR_SORT_DESCENDING); +$lastFile = $files[0]; + +$lastDate = str_replace($str2strip, "", $lastFile); + +$data = array(); +$data['firstDate'] = $firstDate; +$data['lastDate'] = $lastDate; +$data['idp'] = isset($_GET["idp"]) ? htmlspecialchars($_GET["idp"]) : ""; +$data['reg_auth'] = isset($_GET["reg_auth"]) ? htmlspecialchars($_GET["reg_auth"]) : ""; +$data['date'] = isset($_GET["date"]) ? htmlspecialchars($_GET["date"]) : $lastDate; +$data['status'] = isset($_GET["status"]) ? htmlspecialchars($_GET["status"]) : ""; +$data['check_result'] = isset($_GET["check_result"]) ? htmlspecialchars($_GET["check_result"]) : ""; + +echo $twig->render('eccs.html', $data); + +?> diff --git a/templates/eccs.html b/templates/eccs.html new file mode 100644 index 0000000000000000000000000000000000000000..81fd0fafedc63cb3375b46a89d036ea55abdcb2c --- /dev/null +++ b/templates/eccs.html @@ -0,0 +1,73 @@ +{% extends 'common/master.html' %} +{% set breadcrumb = ['Tools', 'eduGAIN Connectivity Check Service'] %} +{% block title %}eduGAIN Connectivity Check Service{% endblock title %} + +{% block additional_head %} + <meta charset=utf-8 /> + <script type="text/javascript" src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script> + <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css"/> + <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"/> + <link rel="stylesheet" type="text/css" href="css/eccs.css" /> + <script> + $( function() { + $( "#datepicker" ).datepicker({ + dateFormat: "yy-mm-dd", + minDate: "{{ firstDate }}", + maxDate: "{{ lastDate }}", + defaultDate: "{{ date }}" + }).datepicker("setDate","{{ date }}"); + } ); + </script> +{% endblock %} + +{% block main_body %} +<div class="eccs-central"> + <h1><a href="/eccs" target="_self">eduGAIN Connectivity Check Service</a> (<a href="https://wiki.geant.org/display/eduGAIN/eduGAIN+Connectivity+Check">Instructions</a>, <a href="mailto:support@edugain.org">Contacts</a>)</h1> + <p>The purpose of the eduGAIN Connectivity Check is to identify eduGAIN Identity Providers (IdP) that does not properly consume eduGAIN SAML2 SP metadata.</p> + <div id="status"> + <hr> + <div class="clearfix"> + <div class="boxStatus"> + <strong>Show IdPs with status:</strong> + <label id="lbl-error" for="error">ERROR</label> + <input id="error" type="checkbox" name="status" value="ERROR"/> + <label id="lbl-ok" for="ok">OK</label> + <input id="ok" type="checkbox" name="status" value="OK"/> + <label id="lbl-unknown" for="unknown">UNKNOWN</label> + <input id="unknown" type="checkbox" name="status" value="UNKNOWN"/> + <label id="lbl-disabled" for="disabled">DISABLED</label> + <input id="disabled" type="checkbox" name="status" value="DISABLE"/> + </div> <!-- END boxStatus --> + <div class="boxCalendar"> + <div id="calendarGo"> + <button id="goButton" onclick="getPastResults()">Go</button> + <label id="lbl-datepicker" for="datepicker" class="strong">Select date:</label> + <input type="text" id="datepicker" /> + </div> <!-- END calendarGo --> + </div> <!-- END boxCalendar --> + </div> <!-- END clearFix --> + <hr> + </div> <!-- END status --> + <div class="container"> + <div class="loader"></div> + <table id="eccstable" class="cell-border" style="width:100%"> + <thead> + <tr> + <th></th> + <th>DisplayName</th> + <th>EntityID</th> + <th>Registration Authority</th> + </tr> + </thead> + </table> + </div> <!-- END container --> + <script type="text/javascript"> + var date = "{{ date }}"; + var reg_auth = "{{ reg_auth }}"; + var idp = "{{ idp }}"; + var status = "{{ status }}"; + var check_result = "{{ check_result }}"; + </script> + <script type="text/javascript" src="js/eccs.js" /></script> + </div> <!-- END eccs-central --> +{% endblock main_body %} diff --git a/web/css/eccs.css b/web/css/eccs.css new file mode 100644 index 0000000000000000000000000000000000000000..c8f2495369d516fe49d1943c91ad5a53b5e3725c --- /dev/null +++ b/web/css/eccs.css @@ -0,0 +1,228 @@ +td.details-control { + background: url('../images/details_open.png') no-repeat center center; + cursor: pointer; +} + +tr.shown td.details-control { + background: url('../images/details_close.png') no-repeat center center; +} + +#lbl-error, #lbl-ok, #lbl-disabled, #lbl-unknown { + position: relative; + top: -1px; + padding: 5px; + color: black; + margin-left: 4px; +} + +#lbl-error { + background-color: #EA3D3F; +} + +#lbl-ok { + background-color: #72F81B; +} + +#lbl-unknown { + background-color: #FFDB58; +} + +#lbl-disabled { + margin-left: 2px; +} + +#inner-table { + padding-left:40px; + background-color:white; +} + +#inner-table tr td{ + border: 0; +} + +table { + color: black; +} + +.eccs-central { + padding-left: 10px; + padding-right: 10px; +} + +.strong { + font-weight: bold; +} + +div#calendarGo { + float:right; + position: relative; + top: 2px; +} + + +button#goButton { + float:right; + padding-right: 10px; + padding-left: 10px; + padding-bottom: 3px; + padding-top: 3px; + margin-left: 5px; + margin-top: 1px; + position: relative; + bottom: 1px; +} + +input[type=checkbox] { + transform: scale(1.5); +} + +#error, #ok, #disabled, #unknown { + position: relative; + bottom: -2px; +} + +#datepicker { + padding: 3px; +} + +* { + box-sizing: border-box; +} + +.boxStatus { + float: left; + width: 50%; + padding: 5px; +} + +.boxCalendar { + float: left; + width: 50%; +} + +.clearfix::after { + content: ""; + clear: both; + display: table; +} + +/* CSS Rules for Mozilla Firefox */ +@-moz-document url-prefix() { + + div#calendarGo { + float:right; + position: relative; + top: 3px; + } + + button#goButton { + float:right; + padding-right: 0; + padding-left: 0; + padding-bottom: 2px; + padding-top: 0; + margin-left: 5px; + position: relative; + bottom: 1px; + } +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted #ccc; + color: #004360; +} + +.tooltip .tooltiptext { + visibility: hidden; + position: absolute; + background-color: #555; + color: #fff; + text-align: center; + padding: 5px 0; + border-radius: 6px; + z-index: 1; + opacity: 0; + transition: opacity .6s; +} + +.tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +} + +.tooltip-top { + bottom: 125%; + left: 50%; +} + +.tooltip-top::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #555 transparent transparent transparent; +} + +.tooltip-ok { + width: 540px; + margin-left: -270px; +} + +.tooltip-timeout { + width: 380px; + margin-left: -190px; +} + +.tooltip-unable-to-check { + width: 170px; + margin-left: -83px; +} + +.tooltip-connection-error { + width: 230px; + margin-left: -100px; +} + +.tooltip-no-sp-metadata-error { + width: 400px; + margin-left: -200px; +} + +.tooltip-idp-generic-error { + width: 180px; + margin-left: -84px; +} + +.tooltip-403-forbidden { + width: 180px; + margin-left: -84px; +} + +.tooltip-ssl-error { + width: 280px; + margin-left: -140px; +} + +.tooltip-disabled { + width: 260px; + margin-left: -130px; +} + +.loader { + background: url('./eccs-loading.gif') no-repeat center center; + position: absolute; + width: 100%; + height: 80%; + background-color: white; + padding-top: 150px; + padding-left: 50%; + z-index: 5; + opacity: 0.6; + display: none; + background-size: 100px 100px; +} diff --git a/web/images/details_close.png b/web/images/details_close.png new file mode 100644 index 0000000000000000000000000000000000000000..9a942ce400589c1c7b726fc0b99577b45c7b5b3f Binary files /dev/null and b/web/images/details_close.png differ diff --git a/web/images/details_open.png b/web/images/details_open.png new file mode 100644 index 0000000000000000000000000000000000000000..2a5b5ca4af8c43d9e2e903426606e54416cb32a7 Binary files /dev/null and b/web/images/details_open.png differ diff --git a/web/js/eccs.js b/web/js/eccs.js new file mode 100644 index 0000000000000000000000000000000000000000..b07c2c5dc15de7f0f84e1623cb31de84d5fa2bcf --- /dev/null +++ b/web/js/eccs.js @@ -0,0 +1,378 @@ +// Needed to draw the ECCS2 DataTable +var table; +var url = "/eccs/api/eccsresults?eccsdt=1"; +var infoCircle = '<a href="https://wiki.geant.org/display/eduGAIN/eduGAIN+Connectivity+Check#eduGAINConnectivityCheck-Statusesandresults"><i class="fas fa-info-circle"></i></a>'; + +/* + * Secure Hash Algorithm (SHA1) + * https://www.webtoolkit.info/javascript_sha1.html +*/ +function SHA1(msg) { + function rotate_left(n,s) { + var t4 = ( n<<s ) | (n>>>(32-s)); + return t4; + }; + function lsb_hex(val) { + var str=''; + var i; + var vh; + var vl; + for( i=0; i<=6; i+=2 ) { + vh = (val>>>(i*4+4))&0x0f; + vl = (val>>>(i*4))&0x0f; + str += vh.toString(16) + vl.toString(16); + } + return str; + }; + function cvt_hex(val) { + var str=''; + var i; + var v; + for( i=7; i>=0; i-- ) { + v = (val>>>(i*4))&0x0f; + str += v.toString(16); + } + return str; + }; + function Utf8Encode(string) { + string = string.replace(/\r\n/g,'\n'); + var utftext = ''; + for (var n = 0; n < string.length; n++) { + var c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } + else if((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + }; + var blockstart; + var i, j; + var W = new Array(80); + var H0 = 0x67452301; + var H1 = 0xEFCDAB89; + var H2 = 0x98BADCFE; + var H3 = 0x10325476; + var H4 = 0xC3D2E1F0; + var A, B, C, D, E; + var temp; + msg = Utf8Encode(msg); + var msg_len = msg.length; + var word_array = new Array(); + for( i=0; i<msg_len-3; i+=4 ) { + j = msg.charCodeAt(i)<<24 | msg.charCodeAt(i+1)<<16 | + msg.charCodeAt(i+2)<<8 | msg.charCodeAt(i+3); + word_array.push( j ); + } + switch( msg_len % 4 ) { + case 0: + i = 0x080000000; + break; + case 1: + i = msg.charCodeAt(msg_len-1)<<24 | 0x0800000; + break; + case 2: + i = msg.charCodeAt(msg_len-2)<<24 | msg.charCodeAt(msg_len-1)<<16 | 0x08000; + break; + case 3: + i = msg.charCodeAt(msg_len-3)<<24 | msg.charCodeAt(msg_len-2)<<16 | msg.charCodeAt(msg_len-1)<<8 | 0x80; + break; + } + word_array.push( i ); + while( (word_array.length % 16) != 14 ) word_array.push( 0 ); + word_array.push( msg_len>>>29 ); + word_array.push( (msg_len<<3)&0x0ffffffff ); + for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ) { + for( i=0; i<16; i++ ) W[i] = word_array[blockstart+i]; + for( i=16; i<=79; i++ ) W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1); + A = H0; + B = H1; + C = H2; + D = H3; + E = H4; + for( i= 0; i<=19; i++ ) { + temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B,30); + B = A; + A = temp; + } + for( i=20; i<=39; i++ ) { + temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B,30); + B = A; + A = temp; + } + for( i=40; i<=59; i++ ) { + temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B,30); + B = A; + A = temp; + } + for( i=60; i<=79; i++ ) { + temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; + E = D; + D = C; + C = rotate_left(B,30); + B = A; + A = temp; + } + H0 = (H0 + A) & 0x0ffffffff; + H1 = (H1 + B) & 0x0ffffffff; + H2 = (H2 + C) & 0x0ffffffff; + H3 = (H3 + D) & 0x0ffffffff; + H4 = (H4 + E) & 0x0ffffffff; + } + var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); + + return temp.toLowerCase(); +} + + +// PHP Variables retrieved from eccs.php +// idp (entityID of the IdP) +// date (date time of the check) +// reg_auth (the IdP RegistrationAuthority) +// status (the ECCS IdP Status) +// check_result (the ECCS check result) +if (date) { + url = url.concat("&date=" + date); +} +if (reg_auth) { + url = url.concat("®_auth=" + reg_auth); +} +if (idp) { + url = url.concat("&idp=" + idp); +} +if (status) { + url = url.concat("&status=" + status); +} +if (check_result) { + url = url.concat("&check_result=" + check_result); +} + +function getPastResults() { + let checkDate = $.datepicker.formatDate("yy-mm-dd", $('#datepicker').datepicker().datepicker('getDate')); + let getUrl = window.location; + let baseUrl = getUrl.protocol + "//" + getUrl.host + "/"; + let dataSource = baseUrl + "/eccs/api/eccsresults?eccsdt=1&date=" + checkDate; + $('.loader').css('display','block'); + table.clear().draw(); + table.ajax.url(dataSource).load(hideLoder); + function hideLoder() { + $('.loader').css('display','none'); + } +} + +// use URL constructor and return hostname +function getHostname(url) { + if (url == ""){ + return null + } + const urlNew = new URL(url); + if (urlNew.hostname){ + return urlNew.hostname; + } + else { + return url.replace(/.+:/g, ''); + } +} + +function getCheckResult(checkResult){ + if (checkResult == "OK"){ + return '<div class="tooltip">OK <span class="tooltiptext tooltip-top tooltip-ok">The IdP is consuming correctly the SP metadata and returns a valid login page</span></div> '+infoCircle; + } + else if (checkResult == "Timeout"){ + return '<div class="tooltip">Timeout <span class="tooltiptext tooltip-top tooltip-timeout">The IdP does not load a valid login page within 60 seconds</span></div> '+infoCircle; + } + else if (checkResult == "Unable-To-Check"){ + return '<div class="tooltip">Unable To Check <span class="tooltiptext tooltip-top tooltip-unable-to-check">The IdP can\'t be checked</span></div> '+infoCircle; + } + else if (checkResult == "Connection-Error"){ + return '<div class="tooltip">Connection Error <span class="tooltiptext tooltip-top tooltip-connection-error">Check failed due a connection error</span></div> '+infoCircle; + } + else if (checkResult == "No-SP-Metadata-Error"){ + return '<div class="tooltip">No-SP-Metadata-Error <span class="tooltiptext tooltip-top tooltip-no-sp-metadata-error">The IdP is not consuming correctly the SP metadata</span></div> '+infoCircle + } + else if (checkResult == "IdP-Generic-Error"){ + return '<div class="tooltip">IdP-Generic-Error <span class="tooltiptext tooltip-top tooltip-idp-generic-error">The IdP reported an error</span></div> '+infoCircle + } + else if (checkResult == "403-Forbidden"){ + return '<div class="tooltip">403-Forbidden <span class="tooltiptext tooltip-top tooltip-403-forbidden">The IdP reported a "403 Forbidden" error</span></div> '+infoCircle + } + else if (checkResult == "SSL-Error"){ + return '<div class="tooltip">SSL-Error <span class="tooltiptext tooltip-top tooltip-ssl-error">The IdP has a problem on its SSL certificate</span></div> '+infoCircle; + } + else if (checkResult == "DISABLED"){ + return '<div class="tooltip">Disabled <span class="tooltiptext tooltip-top tooltip-disabled">The check has been disabled for the IdP</span></div> '+infoCircle; + } + else{ + return checkResult; + } +} + +/* Formatting function for row details - modify as you need */ +function format ( d ) { + // `d` is the original data object for the row + return '<table id="inner-table">'+ + '<tr>'+ + '<td class="strong">IdP DisplayName:</td>'+ + '<td>'+d.displayName+'</td>'+ + '</tr>'+ + '<tr>'+ + '<td class="strong">Technical Contacts:</td>'+ + '<td>'+d.contacts.technical+'</td>'+ + '</tr>'+ + '<tr>'+ + '<td class="strong">Support Contacts:</td>'+ + '<td>'+d.contacts.support+'</td>'+ + '<td class="strong">Check Time</td>'+ + '<td class="strong">Check Result</td>'+ + //'<td class="strong">HTTP Code</td>'+ + '<td class="strong">Page Source</td>'+ + '<td class="strong">Retry Check</td>'+ + '</tr>'+ + '<tr>'+ + '<td class="strong">SP1:</td>'+ + '<td>https://'+getHostname(d.sp1.entityID)+'</td>'+ + '<td>'+d.sp1.checkTime+'</td>'+ + '<td>'+getCheckResult(d.sp1.checkResult)+'</td>'+ + //'<td>'+d.sp1.httpCode+'</td>'+ + '<td><a href="/eccs/html/'+d.date+'/'+SHA1(d.entityID)+'---'+getHostname(d.sp1.entityID)+'.html" target="_blank">Click to open</a></td>'+ + '<td><a href="/eccs/api/getsamlreq?idp='+d.entityID+'&sp='+d.sp1.entityID+'" target="_blank">Click to retry</a></td>'+ + '</tr>'+ + '<tr>'+ + '<td class="strong">SP2:</td>'+ + '<td>https://'+getHostname(d.sp2.entityID)+'</td>'+ + '<td>'+d.sp2.checkTime+'</td>'+ + '<td>'+getCheckResult(d.sp2.checkResult)+'</td>'+ + //'<td>'+d.sp2.httpCode+'</td>'+ + '<td><a href="/eccs/html/'+d.date+'/'+SHA1(d.entityID)+'---'+getHostname(d.sp2.entityID)+'.html" target="_blank">Click to open</a></td>'+ + '<td><a href="/eccs/api/getsamlreq?idp='+d.entityID+'&sp='+d.sp2.entityID+'" target="_blank">Click to retry</a></td>'+ + '</tr>'+ + '<tr>'+ + '<td class="strong">SP3:</td>'+ + '<td>https://'+getHostname(d.sp3.entityID)+'</td>'+ + '<td>'+d.sp3.checkTime+'</td>'+ + '<td>'+getCheckResult(d.sp3.checkResult)+'</td>'+ + //'<td>'+d.sp3.httpCode+'</td>'+ + '<td><a href="/eccs/html/'+d.date+'/'+SHA1(d.entityID)+'---'+getHostname(d.sp3.entityID)+'.html" target="_blank">Click to open</a></td>'+ + '<td><a href="/eccs/api/getsamlreq?idp='+d.entityID+'&sp='+d.sp3.entityID+'" target="_blank">Click to retry</a></td>'+ + '</tr>'+ + '</table>'; +} + +$(document).ready(function() { + // Setup - add a text input to each footer cell + $('#eccstable thead tr').clone(true).appendTo( '#eccstable thead' ); + $('#eccstable thead tr:not(:eq(1)) th').each( function (i) { + var title = $('#eccstable thead th').eq( $(this).index() ).text(); + if($(this).index() !=0 && $(this).index() !=5) $(this).html( '<input type="text" placeholder="Search '+title+'" style="text-align:center;width: 100%;" />' ); + + $( 'input', this ).on( 'keyup change', function () { + if ( table.column(i).search() !== this.value ) { + table + .column(i) + .search( this.value ) + .draw(); + } + } ); + } ); + + table = $('#eccstable').DataTable( { + "responsive": "true", + "ajax": { + "url": url, + "dataSrc": "" + }, + "lengthMenu": [[10, 30, 50, 100, -1], [10, 30, 50, 100, "All"]], + "autoWidth": false, + "dom": '<"top"lip>rt<"bottom"><"clear">', + "columns": [ + { + "className": 'details-control', + "orderable": false, + "data": null, + "defaultContent": '' + }, + { + "data": "displayName", + "defaultContent": '' + }, + { "data": "entityID" }, + { "data": "registrationAuthority" }, + { + "data": "date", + "width": "180px", + "className": "dt-body-center", + "visible": false + }, + { + "data": "status", + "className": "dt-body-center", + "visible": false + } + ], + "rowCallback": function( row, data, index ) { + if (data.status == "ERROR") { + //$('td', row).css('background-color', '#EA4335'); // NEW ECCS2 + $('td', row).css('background-color', '#EA3D3F'); // OLD ECCS + //$('td', row).css('background-color', '#FF0000'); + //$('td', row).css('background-color', '#F22422'); + } + if (data.status == "DISABLED") { + $('td', row).css('background-color', '#FFFFFF'); + } + if (data.status == "OK") { + //$('td', row).css('background-color', '#34A853'); + //$('td', row).css('background-color', '#00CE00'); // NEW ECCS2 + $('td', row).css('background-color', '#72F81B'); // OLD ECCS + } + if (data.status == "UNKNOWN") { + $('td', row).css('background-color', '#FFDB58'); + } + + }, + "order": [[1, 'asc']] + } ); + + // Add event listener for opening and closing details + $('#eccstable tbody').on('click', 'td.details-control', function () { + var tr = $(this).closest('tr'); + var row = table.row( tr ); + + if ( row.child.isShown() ) { + // This row is already open - close it + row.child.hide(); + tr.removeClass('shown'); + } + else { + // Open this row + row.child( format(row.data()) ).show(); + tr.addClass('shown'); + } + } ); + + $('input:checkbox').on('change', function () { + //build a regex filter string with an or(|) condition + var sts = $('input:checkbox[name="status"]:checked').map(function() { + return this.value; + }).get().join('|'); + + //filter in column 5, with an regex, no smart filtering, not case sensitive + table.column(5).search(sts, true, false, false).draw(false); + }); +} );