diff --git a/README.md b/README.md index dd9afd1598aa3a3b32f797b560c9e272e7542865..1ca94b0b52166ef80abad7d8fcecdb7193590caa 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 47a6ce302178d32c14c001c48dd4b117bed529dc..50dfb746da334f06d2178504eb9d43f1e002db33 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 cc84023ddc3dafd8ab4cdd92bccb6ffe4c3ea2d7..2b73888c1f637d1e21fe65002ce68f50e69f0df7 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 c9591301ae1cf1cdd316a5d8980f9b064ebfdfd5..aa9daf2a3a8f409625ca4203245aca200495eed2 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 4422f09c2948d867a2e3d5987c18b384971049d0..0f2ab2a1fa94e420e7510c3af1d234e5c199c6d2 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 0b6051803aef8fbae63f43de54b836adb3d9058a..ce96ebc8428696f38cf3fac1619dfd42789077af 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 6671c5545f058ba15779f998b45ad6fd9d8a3983..ae900759fc3ee56c95eb22b74de35df76f44adcb 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 Binary files /dev/null and b/web/eccs-loading.gif differ diff --git a/web/eccs.css b/web/eccs.css index deb16b4bdd51924be9193e77043f23e5ff1cd0dc..8a465bd5448b5982ab1d83653da3b82465f49bda 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 c3a79c49c0b2ac1432acf3bab277cd72478a4836..57152eb7095424be7d807f7b08a04bca322abeb6 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 befae70f1ca8e37b5155693da23abf78f544be0a..b49fa727a816d557b9eae61c6d7675311c2b1da2 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>