From df2c8be01d9bb0c0903b7c04e08e36ef13e71bcc Mon Sep 17 00:00:00 2001 From: Marco Malavolti <marco.malavolti@gmail.com> Date: Wed, 27 Apr 2022 17:59:26 +0200 Subject: [PATCH] sps-metadata,API,update req,eccs-loading.git,Unable-To-Check --- README.md | 184 ++++++++++++++++++++---------------- api.py | 62 ++++++++---- eccs.py | 45 ++++++--- eccs_properties.py.template | 74 +++++++++++++-- requirements.txt | 12 +-- runEccs.py | 10 +- utils.py | 144 +++++++++++++++++----------- web/eccs-loading.gif | Bin 0 -> 36044 bytes web/eccs.css | 29 +++++- web/eccs.js | 61 +++++++----- web/index.php | 95 ++++++++++--------- 11 files changed, 447 insertions(+), 269 deletions(-) create mode 100644 web/eccs-loading.gif diff --git a/README.md b/README.md index dd9afd1..1ca94b0 100644 --- a/README.md +++ b/README.md @@ -23,60 +23,64 @@ * [Configure](#configure-1) * [Utility](#utility) 10. [ECCS API JSON](#eccs-api-json) -11. [Utility for web interface](#utility-for-web-interface) -12. [Utility for developers](#utility-for-developers) +11. [User Interface](#user-interface) + * [User interface parameters](#user-interface-parameters) +12. [Utility for web interface](#utility-for-web-interface) +13. [Utility for developers](#utility-for-developers) * [ECCS API Development Server](#eccs-api-development-server) -13. [Authors](#authors) +14. [Authors](#authors) -# Introduction +## Introduction -The purpose of the eduGAIN Connectivity Check is to identify eduGAIN Identity Providers (IdP) that are not properly configured. In particular it checks if an IdP properly loads and consumes SAML2 metadata which contains the eduGAIN Service Providers (SP). The check results are published on the public eduGAIN Connectivity Check web page (### NOT-AVAILABLE-YET ###). The main purpose is to increase the service overall quality and user experience of the eduGAIN interfederation service by making federation and Identity Provider operators aware of configuration problems. +The purpose of the eduGAIN Connectivity Check is to identify eduGAIN Identity Providers (IdP) that are not properly configured. In particular it checks if an IdP properly loads and consumes SAML2 metadata which contains the eduGAIN Service Providers (SP). The check results are published on the public eduGAIN Connectivity Check web page ([https://technical.edugain.org/eccs](https://technical.edugain.org/eccs)). The main purpose is to increase the service overall quality and user experience of the eduGAIN interfederation service by making Federation and Identity Provider operators aware of configuration problems. The check is performed by sending a SAML authentication request to each eduGAIN IdP and then follow the various HTTP redirects. The expected result is a login form that allows users to authenticate (typically with username/password) or an error message of some form. For those Identity Providers that output an error message, it can be assumed that they don't consume eduGAIN metadata properly or that they suffer from another configuration problem. There are some cases where the check will generate false positives, therefore IdPs can be excluded from checks as is described below. The Identity Providers are checked once per day. Therefore, the login requests should not have any significant effect on the log entries/statistics of an Identity Provider. Also, no actual login is performed because the check cannot authenticate users due to missing username and password for the IdPs. Only Identity Providers are checked but not the Service Providers. -# Check Performed on the IdPs +## Check Performed on the IdPs -The check executed by the service follows these steps: +The check follows the steps: 1. It retrieves the eduGAIN IdPs from eduGAIN Operator Team database via a JSON interface -2. For each IdP that is was not manually disabled by the eduGAIN Operations Team, the check creates a Wayfless URL for each SP involved and retrieves the IdP login page. It expects to find the HTML form with a username and password field. Therefore, no complete login will happen at the Identity Provider because the check stops at the login page. -The SPs used for the check are "SP Demo provided by GARR" (https://sp-demo.idem.garr.it/shibboleth) from IDEM GARR AAI and the "AAI Viewer Interfederation Test" (https://attribute-viewer.aai.switch.ch/interfederation-test/shibboleth) from SWITCHaai. These SPs might change in the future if needed. -The SAML authenticatin request is not signed. Therefore, authentication request for any eduGAIN SP could be created because the SP's private key is not needed. +2. For each IdP, that hasn't been disabled manually by the eduGAIN Operations Team or dynamically by "robots.txt" (explained below) and that has a valid SSL certificate on its HTTP-Redirect Location, it performs an IdP-initiated SSO with SAML Authentication Request for two SP belonging two different NREN, members of eduGAIN interfederation, and for one fake SP. It expects to find the HTML form with username and password fields on the "good" SPs and an error or other on the "bad" SP. If an IdP uses frames on its Login page, the check follows only the first one on each redirected pages. If an IdP uses HTTP Basic Authentication, the check searches '401 Unauthorized' string into the web page content presented and establish the correct behaviour of the IdP. Therefore, no complete login will happen at the Identity Provider because the check stops at the login page or at SSL Certificate validation. +The SAML authentication request is not signed. Therefore, an authentication request for any eduGAIN SP could be created because the SP's private key is not needed. +The SPs HTTP-Post Assertion Consumer Service URLs used by the check are retrieved by "sps-metadata.xml" into the "input" directory. The 'validation' method used to validate the "sps-metadata.xml" is a deployer decision, but a solution is provided on the "README-SPS-METADATA.md" file. -# Limitations +3. If the check fails for an IdP the first time, it will be checked again at the end of the execution for a second time before exit. + +4. It keeps the results of the last 7 days, but it can be increased by the deployers. + +## Limitations There are some situations where the check cannot work reliably. In those cases it is possible to disable the check for a particular IdP. The so far known cases where the check might generate a false negative are: * IdP does not support HTTP or HTTPS with at least SSLv3 or TLS1 or newer (these IdPs are insecure anyway) * IdP is part of a Hub & Spoke federation (some of them manually have to first approve eduGAIN SPs) -* IdP does not use web-based login form (e.g. HTTP Basic Authentication or X.509 login) +* IdP does not use web-based login form (e.g. Account Chooser Authentication or X.509 login) -# Disable Checks +## Disable Checks -In cases where an IdP cannot be reliably checked, it is necessary to create or enrich the `robots.txt` file on the IdP's web root with: +In cases where an IdP cannot be reliably checked, it is necessary to create or enrich the `robots.txt` file on the IdP's web root dir with: ```bash User-agent: ECCS Disallow: / ``` -If an IdP is not able to create its own `robots.txt` file under the web root directory, it can be disabled by setting the dictionary `IDPS_DISABLED_DICT` into `eccs_properties.py` with a line in the form: +If an IdP is not able to create its own `robots.txt`, it can be disabled by an eduGAIN Operation Team member by setting the dictionary `IDPS_DISABLED_DICT` into `eccs_properties.py` with a line in the form: '<idp-entity-id>':'<eccs-check-disabling-reason>' -# On-line interface - -The test eduGAIN Connectivity Check web pages is available at: https://technical-test.edugain.org/eccs +## On-line interface The tool uses following status for IdPs: * ERROR (red): * The IdP's response contains an HTTP Error or the web page returned does not look like a login page. - * **Invalid-Form**: considers those IdPs that do not load a standard username/password login page and do not return messages like "*No return endpoint available for relying party*" or "*No metadata found for relying party"*. + * **Unable-To-Check**: considers those IdPs that do not load a standard username/password login page and do not return messages like "*No return endpoint available for relying party*" or "*No metadata found for relying party"*. * **Timeout**: considers those IdPs that do not load a standard username/password login page within 60 seconds. * **Connection-Error**: considers those IdPs that are not reachable due to a connection problem. View the "Page Source" value to discover which problem the IdP has. * **IdP-Error**: considers those IdPs that the web page returned does not contain a Login Form and reports an unspecified error such as "*An error occured*". This has been seen on Micrsoft ADFS based IdPs @@ -90,7 +94,7 @@ The tool uses following status for IdPs: * DISABLED (white) * The IdP is excluded because it cannot be checked reliably. The "*Page Source*" column, when an entity is disabled, shows the reason of the disabling. -# Requirements Hardware +## Requirements Hardware * OS: Debian 9, CentOS 7.8 (tested) * HDD: 10 GB @@ -98,23 +102,24 @@ The tool uses following status for IdPs: * CPU: >= 2 vCPU (suggested) * ARCH: 64 Bit -# Requirements Software +## Requirements Software * Apache Server + WSGI -* Python 3 (tested with v3.9.1) -* Selenim + Google Chrome Web Brower (tested with v91.0.4472.164) -* Chromedriver (tested with v91.0.4472.101) +* Python 3 (tested with v3.9.1, v3.10.4) +* Selenim + Google Chrome Web Brower (tested with v91.0.4472.164, v100.0.4896.127) +* Chromedriver (tested with v91.0.4472.101, v100.0.4896.60) * Git +* PHP -# HOWTO Install and Configure +## HOWTO Install and Configure -## Download ECCS Repository +### Download ECCS Repository * `cd $HOME ; git clone https://gitlab.geant.org/edugain/eccs.git` -## Install Python 3 +### Install Python 3 -### CentOS 7 requirements +#### CentOS 7 requirements 1. Update the system packages: * `sudo yum -y update` @@ -125,44 +130,58 @@ The tool uses following status for IdPs: 3. Install needed packages to build python: * `sudo yum-builddep python3` + If you want to use Python 3.10, you need OpenSSL >= 1.1.1: + * `sudo yum install openssl11 openssl11-devel` + * `sudo mkdir /usr/local/openssl11` + * `sudo cd /usr/local/openssl11` + * `sudo ln -s /usr/lib64/openssl11 lib` + * `sudo ln -s /usr/include/openssl11 include` + 4. Install Git: * `sudo yum -y install git` -### Debian requirements +#### Debian requirements 1. Update the system packages: * `sudo apt update ; sudo apt upgrade -y` 2. Install needed packages to build python3: - * `sudo apt-get build-dep python3` + * `sudo apt-get build-dep python3 libffi-dev libssl-dev zlib-dev` 3. Install Git: * `sudo apt install git` -### Install +#### Install 1. Download the last version of Python 3 from https://www.python.org/downloads/source/ into your home: - * `wget https://www.python.org/ftp/python/3.9.1/Python-3.9.1.tgz -O $HOME/eccs/Python-3.9.1.tgz` + * `wget https://www.python.org/ftp/python/3.10.4/Python-3.10.4.tgz -O $HOME/eccs/Python-3.10.4.tgz` 2. Extract Python source package: * `cd $HOME/eccs/` - * `tar xzf Python-3.9.1.tgz` + * `tar xzf Python-3.10.4.tgz` 3. Build Python from the source package: - * `cd $HOME/eccs/Python-3.9.1` - * `./configure --prefix=$HOME/eccs/python` - * `make` + * Debian: + * `cd $HOME/eccs/Python-3.10.4` + * `./configure --prefix=$HOME/eccs/python` + * `make` + + * Centos 7: + * `cd $HOME/eccs/Python-3.10.4` + * `./configure --prefix=$HOME/eccs/python --with-openssl=/usr/local/openssl11` + * `make` 4. Install Python 3 under `$HOME/eccs/python`: * `make install` * `$HOME/eccs/python/bin/python3 --version` + * `$HOME/eccs/python/bin/python3 -c "import ssl; print (ssl.OPENSSL_VERSION)"` - This will install python3 under your $HOME/eccs directory. + This will install python3 under your $HOME/eccs/python directory. 5. Remove useless things: - * `rm -Rf $HOME/eccs/Python-3.9.1 $HOME/eccs/Python-3.9.1.tgz` + * `rm -Rf $HOME/eccs/Python-3.10.4 $HOME/eccs/Python-3.10.4.tgz` -## Install Google Chrome needed by Selenium +### Install Google Chrome needed by Selenium * Debian (64 bit): * `cd $HOME/eccs` @@ -174,32 +193,32 @@ The tool uses following status for IdPs: * `sudo wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm` * `sudo yum install ./google-chrome-stable_current_x86_64.rpm` -## Install the Chromedriver +### Install the Chromedriver 1. Find out which version of Chromium you are using: * Debian 9 (stretch): - * `google-chrome -version` => Google Chrome 91.0.4472.164 + * `google-chrome -version` => Google Chrome 100.0.4896.127 * CentOS 7.8: - * `google-chrome -version` => Google Chrome 91.0.4472.164 + * `google-chrome -version` => Google Chrome 100.0.4896.127 -2. Take the Chrome version number, remove the last part, and append the result to URL "`https://chromedriver.storage.googleapis.com/LATEST_RELEASE_`". For example, with Chrome version 73.0.3683.75, you'd get a URL "`https://chromedriver.storage.googleapis.com/LATEST_RELEASE_73.0.3683`". +2. Take the Chrome version number, remove the last part, and append the result to URL "`https://chromedriver.storage.googleapis.com/LATEST_RELEASE_`". For example, with Chrome version 100.0.4896.127, you'd get a URL "`https://chromedriver.storage.googleapis.com/LATEST_RELEASE_100.0.4896`". -3. Use the URL created in the last step to discover the version of ChromeDriver to use. For example, the above URL will get your a file containing "73.0.3683.68". +3. Use the URL created in the last step to discover the version of ChromeDriver to use. For example, the above URL will get your a file containing "100.0.4896.60". -4. Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version `72.0.3626.68`, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=73.0.3683.68/" +4. Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version `100.0.4896.60`, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=100.0.4896.60/" 5. Download the Chromedriver and extract it with: * `cd $HOME/eccs` - * `wget https://chromedriver.storage.googleapis.com/73.0.3683.68/chromedriver_linux64.zip` + * `wget https://chromedriver.storage.googleapis.com/100.0.4896.60/chromedriver_linux64.zip` * `unzip chromedriver_linux64.zip` * `rm chromedriver_linux64.zip google-chrome-stable_current_amd64.deb` **Note:** After the initial download, it is recommended that you occasionally go through the above process again to see if there are any bug fix releases. -## ECCS Script +### ECCS Script -### Install and Configure the Virtual Environment +#### Install and Configure the Virtual Environment * `cd $HOME/eccs` * `./python/bin/python3 -m pip install virtualenv` @@ -208,7 +227,7 @@ After the initial download, it is recommended that you occasionally go through t * `source eccs-venv/bin/activate` (`deactivate` to exit Virtualenv) * `python3 -m pip install -r requirements.txt` -### Configure ECCS +#### Configure ECCS 1. Configure ECCS properties: * `cp $HOME/eccs/eccs_properties.py.template $HOME/eccs/eccs_properties.py` @@ -250,7 +269,7 @@ After the initial download, it is recommended that you occasionally go through t 0 4 * * * /bin/bash $HOME/eccs/cleanAndRunEccs.sh > $HOME/eccs/logs/eccs-cron.log 2>&1 ``` -## Execute +### Execute * `cd $HOME/eccs` * `./cleanAndRunEccs.py` (to run a full and clean check) @@ -264,9 +283,9 @@ After the initial download, it is recommended that you occasionally go through t The "--test" parameter will not change the result of ECCS, but will write the output on the `logs/stdout_idp_YYYY-MM-DD.log`,`logs/stderr_idp_YYYY-MM-DD.log` and `logs/failed-cmd-idp.sh` files if the argument "--test" will be used. -# ECCS API Server (uWSGI) +## ECCS API Server (uWSGI) -## Install +### Install 1. Install requirements: * Debian: @@ -280,7 +299,7 @@ After the initial download, it is recommended that you occasionally go through t * `sudo restorecon -R -v "$HOME/eccs/html/"` * `sudo setsebool -P httpd_can_network_connect 1` -## Configure +### Configure 1. Add the systemd service to enable ECCS API: * `cd $HOME/eccs` @@ -295,8 +314,10 @@ After the initial download, it is recommended that you occasionally go through t 2. Configure Apache for ECCS web side: * Debian: - * `sudo cp $HOME/eccs/eccs-debian.conf /etc/apache2/conf-available/eccs.conf` + * `sudo cp $HOME/eccs/eccs-debian.conf /etc/apache2/conf-available/eccs.conf + * `sudo vim /etc/apache2/conf-available/eccs.conf` (and change the file opportunely) * `sudo a2enconf eccs.conf` + * `sudo a2enmod proxy_uwsgi` * `sudo chgrp www-data $HOME ; sudo chmod g+rx $HOME` (Apache needs permission to access the $HOME dir) * `sudo systemctl restart apache2.service` * CentOS: @@ -313,13 +334,13 @@ After the initial download, it is recommended that you occasionally go through t 0 3 * * * /usr/bin/touch $HOME/eccs/eccs.ini ``` -## Utility +### Utility To perform a restart after an API change use the following command: * `touch $HOME/eccs/eccs.ini` -# ECCS API JSON +## ECCS API JSON * `/api/eccsresults` (Return the results of the last check ready for ECCS web interface) * `/api/eccsresults?<parameter1>=<value1>&<parameter2>=<value2>`: @@ -332,38 +353,22 @@ To perform a restart after an API change use the following command: * `check_result=` * `OK` * `Timeout` - * `Invalid-Form` * `Connection-Error` * `IdP-Error` * `No-eduGAIN-Metadata` * `SSL-Error` + * `Unable-To-Check` * `DISABLED` * `reg_auth=https://reg.auth.example.org` (select a specific Registration Authority) * `format=simple` (retrieve results in a simple format) * `/api/fedstats` * `/api/fedstats?reg_auth=https://reg.auth.example.org`: -# Utility for web interface - -The available dates are provided by the first and the last file created into the `output/` directory, -remember to change its path into `web/eccs.php` file. - -## Clean old results - -To clean the ECCS results from files older than last 7 days use (modify it on your needs): - -* `crontab -e` - - ```bash - SHELL=/bin/bash - - 0 10 * * * /bin/bash $HOME/eccs/clean7daysOldFiles.sh > $HOME/eccs/logs/clean7daysOldFiles.log 2>&1 - ``` - ## User interface + The eduGAIN Connectivity Check Service web page is available at https://technical-test.edugain.org/eccs -## User interface parameters +### User interface parameters | Parameter name | Example | | -------------- | -------------------------------------------- | @@ -377,24 +382,41 @@ The eduGAIN Connectivity Check Service web page is available at https://technica `https://technical-test.edugain.org/eccs?reg_auth=http://www.idem.garr.it/&check_result=SSL-Error` -# Utility for developers +## Utility for web interface + +The available dates are provided by the first and the last file created into the `output/` directory, +remember to change its path into `web/eccs.php` file. + +### Clean old results + +To clean the ECCS results from files older than last 7 days use (modify it on your needs): + +* `crontab -e` + + ```bash + SHELL=/bin/bash + + 0 10 * * * /bin/bash $HOME/eccs/clean7daysOldFiles.sh > $HOME/eccs/logs/clean7daysOldFiles.log 2>&1 + ``` + +## Utility for developers -## ECCS API Development Server +### ECCS API Development Server * `cd $HOME/eccs ; ./api.py` -## Search files created on the current date +### Search files created on the current date * `cd $HOME/eccs` * `find . -name *$(date +%Y-%m-%d)*` -## Delete files created on the current date +### Delete files created on the current date * `cd $HOME/eccs` * `rm -rf html/$(date +%Y-%m-%d) output/eccs_$(date +%Y-%m-%d).log logs/*_$(date +%Y-%m-%d).log` -# Authors +## Authors -## Original Author +### Original Author * Marco Malavolti (marco.malavolti@garr.it) diff --git a/api.py b/api.py index 47a6ce3..50dfb74 100755 --- a/api.py +++ b/api.py @@ -4,10 +4,12 @@ import json import logging import re -from eccs_properties import DAY, ECCS_LOGSDIR, ECCS_OUTPUTDIR, ECCS_LISTFEDSURL, ECCS_LISTFEDSFILE, ECCS_RESULTSLOG +import eccs_properties as e_p +#from eccs_properties import DAY, ECCS_LOGSDIR, ECCS_OUTPUTDIR, ECCS_LISTFEDSURL, ECCS_LISTFEDSFILE, ECCS_RESULTSLOG from flask import Flask, request, jsonify from flask_restful import Resource, Api -from utils import get_logger, get_list_feds, get_reg_auth_dict +from utils import get_logger, get_list_feds, get_list_eccs_idps, get_reg_auth_dict, generate_login_url +from markupsafe import escape app = Flask(__name__) api = Api(app) @@ -53,7 +55,7 @@ def getSimpleDict(aux): "entityID": aux['entityID'], "registrationAuthority": aux['registrationAuthority'], "status": aux['status'], - "checkResult": [aux["sp1"]["checkResult"],aux["sp2"]["checkResult"]] + "checkResult": [aux["sp1"]["checkResult"],aux["sp2"]["checkResult"],aux["sp3"]["checkResult"]] } return simpleDict @@ -70,8 +72,8 @@ class Test(Resource): class EccsResults(Resource): def get(self): - file_path = f"{ECCS_OUTPUTDIR}/{ECCS_RESULTSLOG}" - date = DAY + file_path = f"{e_p.ECCS_OUTPUTDIR}/{e_p.ECCS_RESULTSLOG}" + date = e_p.DAY status = None idp = None reg_auth = None @@ -83,11 +85,11 @@ class EccsResults(Resource): eccsDataTable = True if 'date' in request.args: date = request.args['date'] - file_path = f"{ECCS_OUTPUTDIR}/eccs_{date}.log" + file_path = f"{e_p.ECCS_OUTPUTDIR}/eccs_{date}.log" if 'status' in request.args: status = request.args['status'].upper() if (status not in ['OK','DISABLED','ERROR']): - return jsonify(error="Incorrect status provided. It can be 'ok','disabled','error'") + return jsonify(error="Incorrect status provided. It can be 'OK','DISABLED','ERROR'") if 'idp' in request.args: idp = request.args['idp'] if (not existsInFile(file_path, idp, "entityID", eccsDataTable, date)): @@ -101,8 +103,8 @@ class EccsResults(Resource): simple = True if 'check_result' in request.args: check_result = request.args['check_result'] - if (check_result not in ['OK','Timeout','Invalid-Form','Connection-Error','No-eduGAIN-Metadata','SSL-Error','IdP-Error','DISABLED']): - return jsonify(error="Incorrect check_result value provided. It can be 'OK','Timeout','Invalid-Form','Connection-Error','No-eduGAIN-Metadata','SSL-Error','IdP-Error' or 'DISABLED'") + if (check_result not in ['OK','Timeout','Unable-To-Check','Connection-Error','No-eduGAIN-Metadata','SSL-Error','IdP-Error','DISABLED']): + return jsonify(error="Incorrect check_result value provided. It can be 'OK','Timeout','Unable-To-Check','Connection-Error','No-eduGAIN-Metadata','SSL-Error','IdP-Error' or 'DISABLED'") lines = [] results = [] @@ -154,6 +156,13 @@ class EccsResults(Resource): results.append(auxsimple) else: results.append(aux) + elif (status and check_result): + if (status == aux['status'] and (check_result == aux['sp1']['checkResult'] or check_result == aux['sp2']['checkResult'])): + if (simple): + auxsimple = getSimpleDict(aux) + results.append(auxsimple) + else: + results.append(aux) elif (idp): if (idp == aux['entityID']): if (simple): @@ -195,17 +204,17 @@ class EccsResults(Resource): # /api/fedstats class FedStats(Resource): def get(self): - list_feds = get_list_feds(ECCS_LISTFEDSURL, ECCS_LISTFEDSFILE) + list_feds = get_list_feds(e_p.ECCS_LISTFEDSURL, e_p.ECCS_LISTFEDSFILE) regAuthDict = get_reg_auth_dict(list_feds) - file_path = f"{ECCS_OUTPUTDIR}/{ECCS_RESULTSLOG}" - date = DAY + file_path = f"{e_p.ECCS_OUTPUTDIR}/{e_p.ECCS_RESULTSLOG}" + date = e_p.DAY reg_auth = None eccsDataTable = False if ('date' in request.args): date = request.args['date'] - file_path = f"{ECCS_OUTPUTDIR}/eccs_{date}.log" + file_path = f"{e_p.ECCS_OUTPUTDIR}/eccs_{date}.log" if ('reg_auth' in request.args): reg_auth = request.args['reg_auth'] if (not existsInFile(file_path, reg_auth, "registrationAuthority", eccsDataTable, date)): @@ -271,17 +280,34 @@ class Help(Resource): def get(self): return { 'ECCS JSON Interface': 'https://wiki.geant.org/display/eduGAIN/eduGAIN+Connectivity+Check+2#eduGAINConnectivityCheck2-JSONinterface' } +@app.route('/getsamlreq') +def getSamlReq(): + # Setup list_eccs_idps + list_eccs_idps = get_list_eccs_idps(e_p.ECCS_LISTIDPSURL, e_p.ECCS_LISTIDPSFILE) + + entityid_idp = request.args.get('idp') + entityid_sp = request.args.get('sp') + + for idp in list_eccs_idps: + if (idp['entityID'] == entityid_idp): + for sp in e_p.ECCS_SPS: + if (sp['entityID'] == entityid_sp): + return f"<meta http-equiv='Refresh' content='0; url={generate_login_url(sp['entityID'], sp['http_post_acs_location'], idp['Location'])}' />" + else: + fake_sp_acs_url = f"{entityid_sp.split('shibboleth')[0]}Shibboleth.sso/SAML2/POST" + return f"<meta http-equiv='Refresh' content='0; url={generate_login_url(entityid_sp,fake_sp_acs_url, idp['Location'])}' />" + # Routes -api.add_resource(Test, '/test') # Route_1 -api.add_resource(EccsResults, '/eccsresults') # Route_2 -api.add_resource(FedStats, '/fedstats') # Route_3 -api.add_resource(Help, '/') # Route_4 +api.add_resource(Help, '/') # Route_1 +api.add_resource(Test, '/test') # Route_2 +api.add_resource(EccsResults, '/eccsresults') # Route_3 +api.add_resource(FedStats, '/fedstats') # Route_4 if __name__ == '__main__': # Useful only for API development Server #app.config['JSON_AS_ASCII'] = True #app.logger.removeHandler(default_handler) - #app.logger = get_logger("eccs_api.log", ECCS_LOGSDIR, "w", "INFO") + #app.logger = get_logger("eccs_api.log", e_p.ECCS_LOGSDIR, "w", "INFO") app.run(port='5002') diff --git a/eccs.py b/eccs.py index cc84023..2b73888 100755 --- a/eccs.py +++ b/eccs.py @@ -2,6 +2,7 @@ import argparse import json +import pdb import sys import utils import eccs_properties as e_p @@ -9,7 +10,7 @@ import eccs_properties as e_p from pathlib import Path """ -The check works with the wayfless url of two SP and successed if the IdP Login Page appears and contains the fields "username" and "password" for each of them. +The check works with the SAML Request generated for three SP (one fake SP and two SP provided by different NREN) and successed if the IdP Login Page appears and contains the fields "username" and "password" for each 'good' SP and not for the fake SP. It is possible to disable the check by eccs_properties IDP_DISABLE_DICT or by "robots.txt" put on the SAMLRequest endpoint root web dir. """ @@ -36,62 +37,76 @@ def store_eccs_result(idp,sp,check_results,idp_status,test): if (test): sys.stdout.write("\nECCS:") - sys.stdout.write('{"displayName":"%s","entityID":"%s","registrationAuthority":"%s","contacts":{"technical":"%s","support":"%s"},"status":"%s","sp1":{"wayflessUrl":"%s","checkTime":"%s","checkResult":"%s"},"sp2":{"wayflessUrl":"%s","checkTime":"%s","checkResult":"%s"}}\n' % ( + sys.stdout.write('{"displayName":"%s","entityID":"%s","registrationAuthority":"%s","contacts":{"technical":"%s","support":"%s"},"status":"%s","sp1":{"entityID":"%s","checkTime":"%s","checkResult":"%s"},"sp2":{"entityID":"%s","checkTime":"%s","checkResult":"%s"},"sp3":{"entityID":"%s","checkTime":"%s","checkResult":"%s"}}\n' % ( get_display_name(idp['displayname']), # IdP-DisplayName idp['entityID'], # IdP-entityID idp['registrationAuthority'], # IdP-RegAuth str_technical_contacts, # IdP-TechCtcsList str_support_contacts, # IdP-SuppCtcsList idp_status, # IdP-ECCS-Status - check_results[0][1], # SP-wayfless-url-1 + check_results[0][1], # SP-entityID-1 check_results[0][2], # SP-check-time-1 check_results[0][3], # SP-check-result-1 - check_results[1][1], # SP-wayfless-url-2 + check_results[1][1], # SP-entityID-2 check_results[1][2], # SP-check-time-2 - check_results[1][3])) # SP-check-result-2 + check_results[1][3], # SP-check-result-3 + check_results[2][1], # SP-entityID-3 + check_results[2][2], # SP-check-time-3 + check_results[2][3] # SP-check-result-3 + ) + ) + else: - # IdP-DisplayName;IdP-entityID;IdP-RegAuth;IdP-tech-ctc-1,IdP-tech-ctc-2;IdP-supp-ctc-1,IdP-supp-ctc-2;IdP-ECCS-Status;SP-wayfless-url-1;SP-check-time-1;SP-result-1;SP-wayfless-url-2;SP-check-time-2;SP-result-2 + # IdP-DisplayName;IdP-entityID;IdP-RegAuth;IdP-tech-ctc-1,IdP-tech-ctc-2;IdP-supp-ctc-1,IdP-supp-ctc-2;IdP-ECCS-Status;SP-entityID-1;SP-check-time-1;SP-result-1;SP-entityID-2;SP-check-time-2;SP-result-2;SP-entityID-3;SP-check-time-3;SP-result-3 with open(f"{e_p.ECCS_OUTPUTDIR}/{e_p.ECCS_RESULTSLOG}", 'a') as f: try: - f.write('{"displayName":"%s","entityID":"%s","registrationAuthority":"%s","contacts":{"technical":"%s","support":"%s"},"status":"%s","sp1":{"wayflessUrl":"%s","checkTime":"%s","checkResult":"%s"},"sp2":{"wayflessUrl":"%s","checkTime":"%s","checkResult":"%s"}}\n' % ( + f.write('{"displayName":"%s","entityID":"%s","registrationAuthority":"%s","contacts":{"technical":"%s","support":"%s"},"status":"%s","sp1":{"entityID":"%s","checkTime":"%s","checkResult":"%s"},"sp2":{"entityID":"%s","checkTime":"%s","checkResult":"%s"},"sp3":{"entityID":"%s","checkTime":"%s","checkResult":"%s"}}\n' % ( get_display_name(idp['displayname']), # IdP-DisplayName idp['entityID'], # IdP-entityID idp['registrationAuthority'], # IdP-RegAuth str_technical_contacts, # IdP-TechCtcsList str_support_contacts, # IdP-SuppCtcsList idp_status, # IdP-ECCS-Status - check_results[0][1], # SP-wayfless-url-1 + check_results[0][1], # SP-entityID-1 check_results[0][2], # SP-check-time-1 check_results[0][3], # SP-check-result-1 - check_results[1][1], # SP-wayfless-url-2 + check_results[1][1], # SP-entityID-2 check_results[1][2], # SP-check-time-2 - check_results[1][3] # SP-check-result-2 + check_results[1][3], # SP-check-result-2 + check_results[2][1], # SP-entityID-3 + check_results[2][2], # SP-check-time-3 + check_results[2][3] # SP-check-result-3 ) ) except IOError: - sys.stderr.write(f"Failed writing result on output file for {idp['entityID']} with {utils.get_label(sp)}.\n\nRun {e_p.ECCS_DIR}/runEccs.py --idp {idp['entityID']} --replace\n") + sys.stderr.write(f"Failed writing result on output file for {idp['entityID']} with {utils.get_label(sp['entityID'])}.\n\nRun {e_p.ECCS_DIR}/runEccs.py --idp {idp['entityID']} --replace\n") sys.exit(1) -# Check an IdP with 2 SPs. +# Check an IdP with 2 SPs + Fake SP. def check(idp,test): check_results = [] + for sp in e_p.ECCS_SPS: result = utils.check_idp_response_selenium(sp,idp,test) if (result): check_results.append(result) else: - sys.stderr.write(f"\nCheck failed for {idp['entityID']} with {utils.get_label(sp)}.\n\nRun {e_p.ECCS_DIR}/runEccs.py --idp {idp['entityID']} --replace\n") + sys.stderr.write(f"\nCheck failed for {idp['entityID']} with {utils.get_label(sp['entityID'])}.\n\nRun {e_p.ECCS_DIR}/runEccs.py --idp {idp['entityID']} --replace\n") sys.exit(1) if (len(check_results) == len(e_p.ECCS_SPS)): check_result_sp1 = check_results[0][3] check_result_sp2 = check_results[1][3] + check_result_sp3 = check_results[2][3] check_result_weberr1 = check_results[0][4] check_result_weberr2 = check_results[1][4] + check_result_weberr3 = check_results[2][4] - # If all checks are 'OK', than the IdP consuming correctly eduGAIN Metadata. - if (check_result_sp1 == check_result_sp2 == "OK"): + # If the result of both good SPs is 'OK' + # and if the result of the fake SP is not 'OK' + # than the IdP is consuming correctly eduGAIN Metadata. + if ((check_result_sp1 == check_result_sp2 == "OK") and check_result_sp3 != "OK"): store_eccs_result(idp,sp,check_results,'OK',test) elif (check_result_sp1 == check_result_sp2 == "DISABLED"): diff --git a/eccs_properties.py.template b/eccs_properties.py.template index c959130..aa9daf2 100644 --- a/eccs_properties.py.template +++ b/eccs_properties.py.template @@ -1,8 +1,49 @@ import os +import random +import string from datetime import date +import xml.etree.ElementTree as ET -DAY = date.today().isoformat() +def get_real_sps(): + sps_list = [] + + namespaces = { + 'md': 'urn:oasis:names:tc:SAML:2.0:metadata', + } + + sp_1_entityid = "https://sp-demo.idem.garr.it/shibboleth" + sp_2_entityid = "https://attribute-viewer.aai.switch.ch/interfederation-test/shibboleth" + + tree = ET.parse(SPS_MD_PATH) + root = tree.getroot() + + sp_1 = root.find(f"./md:EntityDescriptor[@entityID='{sp_1_entityid}']/md:SPSSODescriptor/md:AssertionConsumerService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']", namespaces) + sp_2 = root.find(f"./md:EntityDescriptor[@entityID='{sp_2_entityid}']/md:SPSSODescriptor/md:AssertionConsumerService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']", namespaces) + + sp_1_http_post_acs = sp_1.get("Location") + sp_2_http_post_acs = sp_2.get("Location") + # SP 1 + sps_list.append({ + "entityID":f"{sp_1_entityid}", + "http_post_acs_location":f"{sp_1_http_post_acs}" + }) + + # SP 2 + sps_list.append({ + "entityID":f"{sp_2_entityid}", + "http_post_acs_location":f"{sp_2_http_post_acs}" + }) + + return sps_list + +def get_fake_sp_name(): + chars = string.ascii_lowercase + return ''.join(random.choice(chars) for x in range(10))+'.org' + +# Miscellaneous +DAY = date.today().isoformat() +CA_BUNDLE_PATH = "/etc/ssl/certs/ca-certificates.crt" ECCS_DIR = f"{os.environ['HOME']}/eccs" PATHCHROMEDRIVER = f"{ECCS_DIR}/chromedriver" ECCS_PYTHON = f"{ECCS_DIR}/python/bin/python3" @@ -19,6 +60,9 @@ ECCS_OUTPUTDIR = f"{ECCS_DIR}/output" ECCS_RESULTSLOG = f"eccs_{DAY}.log" ECCS_HTMLDIR = f"{ECCS_DIR}/html" +# SPS Metadata +SPS_MD_PATH = f"{ECCS_INPUTDIR}/sps-metadata.xml" + # Selenium ECCS_SELENIUMDEBUG = False ECCS_SELENIUMLOGDIR = f"{ECCS_DIR}/selenium-logs" @@ -36,12 +80,22 @@ ECCS_STDERRIDP = f"{ECCS_LOGSDIR}/stderr_idp_{DAY}.log" ECCS_FAILEDCMDIDP = f"{ECCS_LOGSDIR}/failed-cmd-idp.sh" # Number of processes to run in parallel -ECCS_NUMPROCESSES = 35 +ECCS_NUMPROCESSES = 30 -# The 2 SPs that will be used to test each IdP +# The 3 SPs that will be used to test each IdP ECCS_SPS = [ - "https://sp-demo.idem.garr.it/Shibboleth.sso/Login?entityID=", - "https://attribute-viewer.aai.switch.ch/interfederation-test/Shibboleth.sso/Login?entityID=" + { + "entityID":f"{get_real_sps()[0]['entityID']}", + "http_post_acs_location":f"{get_real_sps()[0]['http_post_acs_location']}", + }, + { + "entityID":f"{get_real_sps()[1]['entityID']}", + "http_post_acs_location":f"{get_real_sps()[1]['http_post_acs_location']}", + }, + { + "entityID":f"https://{get_fake_sp_name()}/shibboleth", + "http_post_acs_location":f"https://{get_fake_sp_name()}/Shibboleth.sso/SAML2/POST", + } ] # ROBOTS.TXT @@ -49,10 +103,10 @@ ROBOTS_USER_AGENT = "ECCS/2.0 (+https://technical.edugain.org/eccs)" # PATTERNS JAVASCRIPT = '"x-my-okta-version"' -IDPERROR = "error\s(has\s)?occur(r)?ed|Error\swhen\sprocessing\s(the\s)?authentication\srequest|The\s(server|system)\sencountered\san\s(internal\s)?error|Internal\sServer\sError|403\sForbidden|Service\sUnavailable|InvalidProfileConfiguration|Unexpected\sSystem\sError|404\s(.\s)?not\sfound|OpenAthens:\s404|On\stapahtunut\svirhe|Unhandled\sexception|Bad\sGateway|Page\sNot\sFound|Δεν\sεπιτρέπεται\sη\sπρόσβαση|tempora(ry|rily)\s(unavailable|error)+|License\serror|n'est\spas\sgérée|Invalid\sRequest|Erreur\s!|Please\sreport\sthis\serror\sto|该网站无法访问|proxy\serror|There\sis\sa\sproblem\swith\syour\saccount" +IDPERROR = "error\s(has\s)?occur(r)?(ed)$|Error\swhen\sprocessing\s(the\s)?authentication\srequest|The\s(server|system)\sencountered\san\s(internal\s)?error|Internal\sServer\sError|403\sForbidden|Service\sUnavailable|InvalidProfileConfiguration|Unexpected\sSystem\sError|404\s(.\s)?not\sfound|OpenAthens:\s404|On\stapahtunut\svirhe|Unhandled\sexception|Bad\sGateway|Page\sNot\sFound|Δεν\sεπιτρέπεται\sη\sπρόσβαση|tempora(ry|rily)\s(unavailable|error)+|License\serror|n'est\spas\sgérée|Invalid\sRequest|Erreur\s!|Please\sreport\sthis\serror\sto|该网站无法访问|proxy\serror|There\sis\sa\sproblem\swith\syour\saccount" METADATAPATTERN = "Unable\sto\slocate(\sissuer\sin|)\smetadata(\sfor|)|no\smetadata\sfound|profile\sis\snot\sconfigured\sfor\srelying\sparty|Cannot\slocate\sentity|fail\sto\sload\sunknown\sprovider|does\snot\srecognise\sthe\sservice|unable\sto\sload\sprovider|Nous\sn'avons\spas\spu\s(charg|charger)\sle\sfournisseur\sde\sservice|Metadata\snot\sfound|application\s(you\shave\saccessed\s)?is\snot\sregistered\s(for\suse\sthis\sservice)?|Message\sdid\snot\smeet\ssecurity\srequirements|unsupported\s[Rr]equest|METADATANOTFOUND|Unknown\slogin\srequester|is\sunspecified\sor\sunsupported|Unknown\sservice\sprovider|Richiesta\snon\ssupportata|Metadati\snon\strovati|untrusted\sprovider|Unregistered\sService|UNHANDLEDEXCEPTION|Metadata.*.expired|Could\snot\sfind\sany.*.metadata.*.for|不支持的请求|l'application\sn'est\spas\senregistrée|Requisição\snão\ssuportada|トされていないリクエスト|is\snot\sallowed|Authorization\sFailure|Pedido\snão\ssuportado|Nicht\sunterstützte\sAnfrage|Service\sNot\sAuthorized\sfor\sSingle\sSign-On|Your\sbrowser\ssent\sa\srequest\sthat\sthis\sserver\scould\snot\sunderstand|Application\sNot\sAuthorized\sTo\sUse\sCAS" -XPATH_CHECK_PATTERN = '//input[@type="password"]|//input[@type="Password"]|//input[@type="email"]|//input[@type="user"]|//input[@name="name"]|//form[@action="/idp/module.php/multiauth/selectsource.php"]' -#PASSWORDPATTERN = '<input[\s]+[^>]*(type=\s*[\'"]password[\'"]|password)[^>]*>' +XPATH_CHECK_PATTERN = '//input[@type="password"]|//input[@type="Password"]|//input[@type="email"]|//input[@type="user"]|//input[@name="name"]|//form[@action="/idp/module.php/multiauth/selectsource.php"]|//input[@type="text"]' +PASSWORDPATTERN = '<input[\s]+[^>]*(type=\s*[\'"]password[\'"]|password)[^>]*>' #USERNAMEPATTERN = '<input[\s]+[^>]*((type=\s*[\'"](text|email)[\'"]|user)|(name=\s*[\'"](name)[\'"]))[^>]*>' #REFUSEDPATTERN = '(^http)(.*\.png$)|(.*\.css$)|(.*\.js$)|(.*\.gif$)|(.*\.svg$)|(.*\.jpg$)' @@ -67,7 +121,7 @@ FEDS_DISABLED_DICT = { IDPS_DISABLED_DICT = { 'https://idp.eie.gr/idp/shibboleth':'Disabled on 2019-04-24 because ECCS cannot check non-standard login page', 'https://edugain-proxy.igtf.net/simplesaml/saml2/idp/metadata.php':'Disabled on 2017-03-17 on request of federation operator', - 'https://gn-vho.grnet.gr/idp/shibboleth':'Disabled on 2019-04-24 because basic authentication is not supported by ECCS check', +# 'https://gn-vho.grnet.gr/idp/shibboleth':'Disabled on 2019-04-24 because basic authentication is not supported by ECCS check', 'https://wtc.tu-chemnitz.de/shibboleth':'Disabled on 2019-02-26 because ECCS cannot check non-standard login page', 'https://idp.fraunhofer.de/idp/shibboleth':'Disabled on 2017-11-24 on request of federation operator', 'https://idp.dfn-cert.de/idp/shibboleth':'Disabled on 2018-04-05 on request of federation operator', @@ -75,7 +129,7 @@ IDPS_DISABLED_DICT = { 'https://login.lstonline.ac.uk/idp/pingfederate':'Disabled on 2017-02-08 on request of federation operator', 'https://indiid.net/idp/shibboleth':'Disabled on 2017-10-27 on request of federation operator', 'https://idp.nulc.ac.uk/openathens':'Disabled on 2017-10-27 on request of federation operator', - 'https://lc-idp.lincolncollege.ac.uk/shibboleth':'Disabled on 2015-08-17 because uses HTTP Basic authentication, which cannot be checked reliably', +# 'https://lc-idp.lincolncollege.ac.uk/shibboleth':'Disabled on 2015-08-17 because uses HTTP Basic authentication, which cannot be checked reliably', 'https://idp.wnsc.ac.uk/idp/shibboleth':'Disabled on 2017-10-27 on request of federation operator', # 'https://idp.strodes.ac.uk/shibboleth':'Disabled on 2015-08-17 because uses HTTP Basic authentication, which cannot be checked reliably', 'https://idp.uel.ac.uk/shibboleth':'Disabled on 2017-10-27 on request of federation operator', diff --git a/requirements.txt b/requirements.txt index 4422f09..0f2ab2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -click==7.1.2 -Flask==1.1.2 +click==8.1.2 +Flask==2.1.1 Flask-RESTful==0.3.9 -requests==2.25.1 -selenium==3.141.0 -uWSGI==2.0.19.1 -urllib3==1.26.6 +requests==2.27.1 +selenium==4.1.3 +urllib3==1.26.9 +uWSGI==2.0.20 diff --git a/runEccs.py b/runEccs.py index 0b60518..ce96ebc 100755 --- a/runEccs.py +++ b/runEccs.py @@ -81,14 +81,10 @@ if __name__=="__main__": start = time.time() # Setup list_feds - url = e_p.ECCS_LISTFEDSURL - dest_file = e_p.ECCS_LISTFEDSFILE - list_feds = utils.get_list_feds(url, dest_file) + list_feds = utils.get_list_feds(e_p.ECCS_LISTFEDSURL, e_p.ECCS_LISTFEDSFILE) # Setup list_eccs_idps - url = e_p.ECCS_LISTIDPSURL - dest_file = e_p.ECCS_LISTIDPSFILE - list_eccs_idps = utils.get_list_eccs_idps(url, dest_file) + list_eccs_idps = utils.get_list_eccs_idps(e_p.ECCS_LISTIDPSURL, e_p.ECCS_LISTIDPSFILE) if (args.idp_entityid): stdout_file = open(e_p.ECCS_STDOUTIDP,"w+") @@ -100,6 +96,8 @@ if __name__=="__main__": cmd = f"{e_p.ECCS_DIR}/eccs.py '{json.dumps(idpJsonList[0])}' --test" elif (args.replace): cmd = f"{e_p.ECCS_DIR}/eccs.py '{json.dumps(idpJsonList[0])}' --replace" + else: + cmd = f"{e_p.ECCS_DIR}/eccs.py '{json.dumps(idpJsonList[0])}'" # List of only one command proc_list = [cmd] diff --git a/utils.py b/utils.py index 6671c55..ae90075 100644 --- a/utils.py +++ b/utils.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 +import base64 import datetime import json import logging import pathlib import re import requests +import six import sys import shutil import time +import uuid +import zlib import eccs_properties as e_p @@ -20,6 +24,7 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.options import Options from logging.handlers import RotatingFileHandler from urllib3.util import parse_url +from urllib.parse import urlparse, urlencode def sha1(idp_entity_id): import hashlib @@ -146,7 +151,7 @@ def store_page_source(page_source,idp,sp,test): return True else: # Put the page_source into an appropriate HTML file - with open(f"{e_p.ECCS_HTMLDIR}/{e_p.DAY}/{sha1(idp['entityID'])}---{get_label(sp)}.html","w") as html: + with open(f"{e_p.ECCS_HTMLDIR}/{e_p.DAY}/{sha1(idp['entityID'])}---{get_label(sp['entityID'])}.html","w") as html: try: html.write(page_source) return True @@ -162,7 +167,7 @@ def get_driver_selenium(idp=None,sp=None,debugSelenium=False): chrome_options = Options() chrome_options.page_load_strategy = 'normal' - chrome_options.add_argument('--start-in-incognito') + #chrome_options.add_argument('--start-in-incognito') chrome_options.add_argument('--headless') chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') @@ -175,7 +180,7 @@ def get_driver_selenium(idp=None,sp=None,debugSelenium=False): # When debugging issues, it is helpful to enable more verbose logging.) if (debugSelenium): label_idp = get_label(idp['entityID']) - label_sp = get_label(sp) + label_sp = get_label(sp['entityID']) sha1_idp = sha1(idp['entityID']) try: driver = webdriver.Chrome(e_p.PATHCHROMEDRIVER, options=chrome_options, service_args=['--verbose', f'--log-path={e_p.ECCS_SELENIUMLOGDIR}/{sha1_idp}_{label_idp}_{label_sp}.log']) @@ -197,12 +202,41 @@ def follow_all_nested_iframes(driver): except NoSuchElementException: return driver.page_source +def deflate_and_base64_encode(string_val): + """ + Deflates and the base64 encodes a string + :param string_val: The string to deflate and encode + :return: The deflated and encoded string + """ + if not isinstance(string_val, six.binary_type): + string_val = string_val.encode('utf-8') + return base64.b64encode(zlib.compress(string_val)[2:-4]) + + +def generate_login_url(sp_entity_id, sp_http_post_acs_location, idp_http_redirect_sso_location): + authn_request_id = f'_{str(uuid.uuid4()).replace("-", "")}' + issue_instant = str(datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds')).replace('+00:00', 'Z') + authn_request = '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' \ + f'AssertionConsumerServiceURL="{sp_http_post_acs_location}" ' \ + f'Destination="{idp_http_redirect_sso_location}" ' \ + f'ID="{authn_request_id}" ' \ + f'IssueInstant="{issue_instant}" ' \ + 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" ' \ + 'Version="2.0">' \ + f'<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{sp_entity_id}</saml:Issuer>' \ + '<samlp:NameIDPolicy AllowCreate="1"/>' \ + '</samlp:AuthnRequest>' + args = {"SAMLRequest": deflate_and_base64_encode(authn_request)} + string = urlencode(args) + glue_char = "&" if urlparse(idp_http_redirect_sso_location).query else "?" + return glue_char.join([idp_http_redirect_sso_location, string]) + # ECCS Check made by Selenium def check_idp_response_selenium(sp,idp,test): # Common variables fqdn_idp = get_label(idp['Location']) - wayfless_url = f"{sp}{idp['entityID']}" + saml_request_url = generate_login_url(sp['entityID'], sp['http_post_acs_location'], idp['Location']) robots = "" federations_disabled_dict = e_p.FEDS_DISABLED_DICT idps_disabled_dict = e_p.IDPS_DISABLED_DICT @@ -213,13 +247,13 @@ def check_idp_response_selenium(sp,idp,test): check_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') + 'Z' page_source = federations_disabled_dict[idp['registrationAuthority']] store_page_source(page_source,idp,sp,test) - return (idp['entityID'],wayfless_url,check_time,"DISABLED",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"DISABLED",webdriver_error) if (idp['entityID'] in idps_disabled_dict.keys()): check_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') + 'Z' page_source = idps_disabled_dict[idp['entityID']] store_page_source(page_source,idp,sp,test) - return (idp['entityID'],wayfless_url,check_time,"DISABLED",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"DISABLED",webdriver_error) # Robots + SSL Check try: @@ -227,7 +261,7 @@ def check_idp_response_selenium(sp,idp,test): 'User-Agent': f'{e_p.ROBOTS_USER_AGENT}' } check_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') + 'Z' - robots = requests.get(f"https://{fqdn_idp}/robots.txt", headers=hdrs, verify=True, timeout=e_p.ECCS_REQUESTSTIMEOUT) + robots = requests.get(f"https://{fqdn_idp}/robots.txt", headers=hdrs, verify=e_p.CA_BUNDLE_PATH, timeout=e_p.ECCS_REQUESTSTIMEOUT) if (robots == ""): robots = requests.get(f"http://{fqdn_idp}/robots.txt", headers=hdrs, verify=False, timeout=e_p.ECCS_REQUESTSTIMEOUT) @@ -238,7 +272,7 @@ def check_idp_response_selenium(sp,idp,test): if (test): page_source = f"\nAn SSL Error occurred while opening https://{fqdn_idp}/robots.txt:\n\n{e}\n\nCheck it on SSL Labs: https://www.ssllabs.com/ssltest/analyze.html?d={fqdn_idp}" else: page_source = f"<h1>SSL ERROR</h1><h2>An SSL error occurred for the server {fqdn_idp}:</h2><p>{e}</p><p>Check it on SSL Labs: <a href='https://www.ssllabs.com/ssltest/analyze.html?d={fqdn_idp}'>Click Here</a></p>" store_page_source(page_source,idp,sp,test) - return (idp['entityID'],wayfless_url,check_time,"SSL-Error",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"SSL-Error",webdriver_error) else: pass @@ -254,7 +288,7 @@ def check_idp_response_selenium(sp,idp,test): if (m): page_source = "<h1>IdP excluded from check by robots.txt</h1>" store_page_source(page_source,idp,sp,test) - return (idp['entityID'],wayfless_url,check_time,"DISABLED",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"DISABLED",webdriver_error) try: # WebDriver MUST be instanced here to avoid problems with SESSION @@ -262,64 +296,64 @@ def check_idp_response_selenium(sp,idp,test): # Exception of WebDriver raises if (driver == None): - sys.stderr.write(f"get_driver_selenium() returned None for IDP {idp['entityID']}(SHA1: {sha1(idp['entityID'])}) with SP {get_label(sp)}") + sys.stderr.write(f"get_driver_selenium() returned None for IDP {idp['entityID']}(SHA1: {sha1(idp['entityID'])}) with SP {get_label(sp['entityID'])}") return None driver.set_page_load_timeout(e_p.ECCS_SELENIUMPAGELOADTIMEOUT) driver.set_script_timeout(e_p.ECCS_SELENIUMSCRIPTTIMEOUT) check_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') + 'Z' - driver.get(wayfless_url) + + driver.get(saml_request_url) + pgsrc = driver.page_source + # Support HTTP Basic Authentication - unauthorized = re.search('401.(\D.|\s.)?Unauthorized', driver.page_source, re.IGNORECASE) + unauthorized = re.search('401.(\D.|\s.)?Unauthorized', pgsrc, re.IGNORECASE) if (unauthorized): - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\n[WAYFLESS URL]{wayfless_url} - JAVASCRIPT FOUND" - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\n[SP] {sp['entityID']} - 401 UNAUTHORIZED FOUND" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"OK",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"OK",webdriver_error) - metadata_not_found = re.search(e_p.METADATAPATTERN,driver.page_source, re.IGNORECASE) + metadata_not_found = re.search(e_p.METADATAPATTERN, pgsrc, re.IGNORECASE) if (metadata_not_found): - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\n[WAYFLESS URL]{wayfless_url} - METADATA NOT FOUND" - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\n[SP] {sp['entityID']} - METADATA NOT FOUND" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"No-eduGAIN-Metadata",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"No-eduGAIN-Metadata",webdriver_error) - idp_error = re.search(e_p.IDPERROR,driver.page_source, re.IGNORECASE) + idp_error = re.search(e_p.IDPERROR, pgsrc, re.IGNORECASE) if (idp_error): - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\n[WAYFLESS URL]{wayfless_url} - IDP ERROR" - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\n[SP] {sp['entityID']} - IDP ERROR" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"IdP-Error",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"IdP-Error",webdriver_error) + + load_js = re.search(e_p.JAVASCRIPT, pgsrc, re.IGNORECASE) + if (load_js): + driver.refresh() # If meet <iframe> follow all iframes - if ('<iframe' in driver.page_source): + if ('<iframe' in pgsrc): pwd_regexp = e_p.PASSWORDPATTERN - pwd_found = re.search(pwd_regexp,driver.page_source, re.IGNORECASE) + pwd_found = re.search(pwd_regexp,pgsrc, re.IGNORECASE) if (not pwd_found): follow_all_nested_iframes(driver) - load_js = re.search(e_p.JAVASCRIPT, driver.page_source, re.IGNORECASE) - if (load_js): - driver.refresh() - WebDriverWait(driver, e_p.ECCS_SELENIUMPAGELOADTIMEOUT).until( EC.presence_of_element_located((By.XPATH,e_p.XPATH_CHECK_PATTERN)) ) - if (test): pgsrc = f"\n[WAYFLESS_URL]\n{wayfless_url} - OK" - else: pgsrc = driver.page_source - stored = store_page_source(pgsrc,idp,sp,test) + if (test): pgsrc = f"\n[SP] {sp['entityID']} - [IDP] {idp['entityID']} - OK" + stored = store_page_source(driver.page_source,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"OK",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"OK",webdriver_error) except TimeoutException as e: - metadata_not_found = re.search(e_p.METADATAPATTERN,driver.page_source, re.IGNORECASE) + pgsrc = driver.page_source + metadata_not_found = re.search(e_p.METADATAPATTERN, pgsrc, re.IGNORECASE) try: input_xpath_found = driver.find_element(By.XPATH, e_p.XPATH_CHECK_PATTERN) @@ -327,67 +361,63 @@ def check_idp_response_selenium(sp,idp,test): except NoSuchElementException as e: # This IF is for those IdP that doesn't consuming the eduGAIN metadata and reaching Timeout if (metadata_not_found): - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\n[WAYFLESS URL]{wayfless_url} - METADATA NOT FOUND" - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\n[SP] {sp['entityID']} - METADATA NOT FOUND" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"No-eduGAIN-Metadata",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"No-eduGAIN-Metadata",webdriver_error) else: try: response = requests.get(f"{driver.current_url}", timeout=e_p.ECCS_REQUESTSTIMEOUT) if (response.status_code == 401): if (test): pgsrc = f"\n[PAGE_SOURCE]\nHTTP Basic Authentication\n[URL]{driver.current_url} - 401 STATUS CODE FOUND" - else: pgsrc = driver.page_source stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"OK",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"OK",webdriver_error) if (response.status_code == 403): if (test): pgsrc = f"\n[PAGE_SOURCE]\nForbidden\n[URL]{driver.current_url} - 403 STATUS CODE FOUND" - else: pgsrc = driver.page_source stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"IdP-Error",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"IdP-Error",webdriver_error) except: pass # ignore all requests exceptions - if (driver.page_source != "<html><head></head><body></body></html>"): - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\nInvalid-Form: No valid login form found in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds." - else: pgsrc = f"<h1>Invalid Form: no valid login form found in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds</h1><h2>PAGE SOURCE:</h2><br/>{driver.page_source}" + # IdPs that do not show a Metadata error after reaching the Timeout and that raise an Exception on the "request" + if (pgsrc != "<html><head></head><body></body></html>" or pgsrc != ""): + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\nUnable-To-Check: ECCS can't check the IdP login." + else: pgsrc = f"<h1>Unable To Check - ECCS can't check the IdP login</h1><h2>IDP LOGIN PAGE SOURCE:</h2><br/>{pgsrc}" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"Invalid-Form",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"Unable-To-Check",webdriver_error) else: - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\nTimeout: No valid login form loaded in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds." - else: pgsrc = f"<h1>Timeout - No valid login form found in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds.</h1><h2>PAGE SOURCE:</h2><br/>{driver.page_source}" + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\nTimeout: No valid login form loaded in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds." + else: pgsrc = f"<h1>Timeout - No valid login form found in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds.</h1>" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"Timeout",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"Timeout",webdriver_error) # Exceptions that are not "NoSuchElementExceptions" except e: - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source}\nTimeout: No valid login form loaded in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds." - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc}\nTimeout: No valid login form loaded in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds." stored = store_page_source(f"<h1>Timeout - No valid login form found in {e_p.ECCS_SELENIUMPAGELOADTIMEOUT} seconds.</h1><br/><p>{pgsrc}</p>",idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"Timeout",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"Timeout",webdriver_error) # input_xpath has been found # This IF is for those IdPs that Timeout is caused by an image or other that do not prevent the Login process. - if (test): pgsrc = f"\n[PAGE_SOURCE]\n{driver.page_source} - Timeout but OK" - else: pgsrc = driver.page_source + if (test): pgsrc = f"\n[PAGE_SOURCE]\n{pgsrc} - Timeout but OK" stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"OK",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"OK",webdriver_error) except WebDriverException as e: error = e.__dict__['msg'].split('(')[0].rstrip() - if (test): pgsrc = f"\nA Connection error occurred while opening {wayfless_url}:\n\n{error}" - else: pgsrc = f"<h1>CONNECTION ERROR</h1><h2>A Connection error occurred while opening <a href='{wayfless_url}'>{wayfless_url}</a>:</h2><p>{error}</p>" + if (test): pgsrc = f"\nA Connection error occurred while opening {generate_login_url(sp['entityID'], sp['http_post_acs_location'], idp['Location'])}:\n\n{error}" + else: pgsrc = f"<h1>CONNECTION ERROR</h1><h2>A Connection error occurred while opening <a href='{generate_login_url(sp['entityID'], sp['http_post_acs_location'], idp['Location'])}'>SAML Request URL</a>:</h2><p>{error}</p>" webdriver_error = 1 stored = store_page_source(pgsrc,idp,sp,test) if (stored): - return (idp['entityID'],wayfless_url,check_time,"Connection-Error",webdriver_error) + return (idp['entityID'],sp['entityID'],check_time,"Connection-Error",webdriver_error) finally: driver.quit() diff --git a/web/eccs-loading.gif b/web/eccs-loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..90843f647d566a8517e61715d7f7ba72a9b05308 GIT binary patch literal 36044 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 literal 0 HcmV?d00001 diff --git a/web/eccs.css b/web/eccs.css index deb16b4..8a465bd 100644 --- a/web/eccs.css +++ b/web/eccs.css @@ -1,10 +1,10 @@ td.details-control { - background: url('../images/details_open.png') no-repeat center center; + background: url('./details_open.png') no-repeat center center; cursor: pointer; } tr.shown td.details-control { - background: url('../images/details_close.png') no-repeat center center; + background: url('./details_close.png') no-repeat center center; } #lbl-error, #lbl-ok, #lbl-disabled { @@ -174,9 +174,14 @@ input[type=checkbox] { margin-left: -190px; } -.tooltip-invalid-form { - width: 260px; - margin-left: -130px; +.tooltip-unable-to-check { + width: 170px; + margin-left: -83px; +} + +.tooltip-connection-error { + width: 230px; + margin-left: -100px; } .tooltip-no-edugain-metadata { @@ -198,3 +203,17 @@ input[type=checkbox] { 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/eccs.js b/web/eccs.js index c3a79c4..57152eb 100644 --- a/web/eccs.js +++ b/web/eccs.js @@ -1,7 +1,7 @@ // Needed to draw the ECCS DataTable var table; var url = "/eccs/api/eccsresults?eccsdt=1"; -var infoCircle = '<a href="https://wiki.geant.org/display/eduGAIN/eduGAIN+Connectivity+Check+2#eduGAINConnectivityCheck2-Statusesandresults"><i class="fas fa-info-circle"></i></a>'; +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) @@ -165,16 +165,16 @@ if (check_result) { } function getPastResults() { - var checkDate = $.datepicker.formatDate("yy-mm-dd", $('#datepicker').datepicker().datepicker('getDate')); - - url = "/eccs/api/eccsresults?eccsdt=1&date=" + checkDate; - $("#datepicker").datepicker("setDate",checkDate); - table.ajax.url( url ).load(); - - var getUrl = window.location; - var baseUrl = getUrl .protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1]; - - document.location.href = baseUrl + "?date=" + checkDate; + 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 @@ -196,23 +196,23 @@ function getCheckResult(checkResult){ return '<div class="tooltip">OK <span class="tooltiptext tooltip-top tooltip-ok">The IdP is consuming correctly the eduGAIN metadata and return 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 30 seconds</span></div> '+infoCircle; + 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 == "Invalid-Form"){ - return '<div class="tooltip">Invalid Form <span class="tooltiptext tooltip-top tooltip-invalid-form">The IdP does not load a valid login page</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-invalid-form">Check failed due a connection error</span></div> '+infoCircle; + 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-eduGAIN-Metadata"){ return '<div class="tooltip">No-eduGAIN-Metadata <span class="tooltiptext tooltip-top tooltip-no-edugain-metadata">The IdP is not consuming correctly edugGAIN metadata stream</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 == "IdP-Error"){ return '<div class="tooltip">IdP-Error <span class="tooltiptext tooltip-top tooltip-idp-error">The IdP reported an 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; } @@ -244,21 +244,30 @@ function format ( d ) { '</tr>'+ '<tr>'+ '<td class="strong">SP1:</td>'+ - '<td>https://'+getHostname(d.sp1.wayflessUrl)+'</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.wayflessUrl)+'.html" target="_blank">Click to open</a></td>'+ - '<td><a href="'+d.sp1.wayflessUrl+'" target="_blank">Click to retry</a></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.wayflessUrl)+'</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.wayflessUrl)+'.html" target="_blank">Click to open</a></td>'+ - '<td><a href="'+d.sp2.wayflessUrl+'" target="_blank">Click to retry</a></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>'; } @@ -316,7 +325,7 @@ $(document).ready(function() { ], "rowCallback": function( row, data, index ) { if (data.status == "ERROR") { - //$('td', row).css('background-color', '#EA4335'); // NEW ECCS + //$('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'); @@ -326,7 +335,7 @@ $(document).ready(function() { } if (data.status == "OK") { //$('td', row).css('background-color', '#34A853'); - //$('td', row).css('background-color', '#00CE00'); // NEW ECCS + //$('td', row).css('background-color', '#00CE00'); // NEW ECCS2 $('td', row).css('background-color', '#72F81B'); // OLD ECCS } }, diff --git a/web/index.php b/web/index.php index befae70..b49fa72 100644 --- a/web/index.php +++ b/web/index.php @@ -24,7 +24,6 @@ $data['check_result'] = htmlspecialchars($_GET["check_result"]); <html> <head> <meta charset=utf-8 /> - <script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.js" crossorigin="anonymous"></script> <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"/> @@ -42,50 +41,56 @@ $data['check_result'] = htmlspecialchars($_GET["check_result"]); <title>eduGAIN Connectivity Check Service</title> </head> <body> - <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-disabled" for="disabled">DISABLED</label> - <input id="disabled" type="checkbox" name="status" value="DISABLE"/> - </div> - <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> - </div> - </div> + <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-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 --> + <button id="btn-show-all-children" type="button">Expand All</button> + <button id="btn-hide-all-children" type="button">Collapse All</button> <hr> - </div> - <button id="btn-show-all-children" type="button">Expand All</button> - <button id="btn-hide-all-children" type="button">Collapse All</button> - <hr> - <div class="container"> - <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> - <script type="text/javascript"> - var date = "<?php echo $data['date'] ?>"; - var reg_auth = "<?php echo $data['reg_auth'] ?>"; - var idp = "<?php echo $data['idp'] ?>"; - var status = "<?php echo $data['status'] ?>"; - var check_result = "<?php echo $data['check_result'] ?>"; - </script> - <script type="text/javascript" src="eccs.js" /></script> + <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 = "<?php echo $data['date'] ?>"; + var reg_auth = "<?php echo $data['reg_auth'] ?>"; + var idp = "<?php echo $data['idp'] ?>"; + var status = "<?php echo $data['status'] ?>"; + var check_result = "<?php echo $data['check_result'] ?>"; + </script> + <script type="text/javascript" src="eccs.js" /></script> + </div> <!-- END eccs-central --> </body> </html> -- GitLab