diff --git a/README.md b/README.md index 19ae16e49b22583d37f6abdcf27bf5f68b813f6e..b87f877f00122a6a20fd0453fb9283e029fd1ad7 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,17 @@ * `sudo apt install python3 python3-pip chromium chromium-l10n git libapache2-mod-wsgi python3-dev` * `python3 -m pip install --user --upgrade pip virtualenv` * `python3 -m venv eccs2venv` -* `source eccs2venv/bin/activate` (`deactivate` di exit Virtualenv) +* `source eccs2venv/bin/activate` (`deactivate` to exit Virtualenv) * `python3 -m pip install --upgrade wheel setuptools certifi selenium urllib3 flask flask-jsonpify flask-restful` * `cd ~ ; git clone https://github.com/malavolti/eccs2.git` * `cd eccs2 ; ./eccs2.py` +# API Development Server + +* `sudo apt install libapache2-mod-wsgi-py3 python3-dev` +* `sudo a2enmod wsgi` +* `cd ~/eccs2 ; ./api.py` + # API * `/eccs/test` (Trivial Test) @@ -23,8 +29,4 @@ * 'Excluded' * /eccs/eccsresults (Return the results of the last check ready for ECCS Gui) -# API Development Server -* `sudo apt install libapache2-mod-wsgi-py3 python3-dev` -* `sudo a2enmod wsgi` -* `cd ~/eccs2 ; ./api.py` diff --git a/api.py b/api.py index b0b3b619c3a4aec22ea7180dfcd1befb3ba6b0b5..b5d8c00a268e31e78b2b9081eeb478e0bbfaa2c4 100755 --- a/api.py +++ b/api.py @@ -8,6 +8,7 @@ from pathlib import PurePath import logging from logging.handlers import RotatingFileHandler import re +import eccs2properties app = Flask(__name__) @@ -67,7 +68,7 @@ class Checks(Resource): def get(self): app.logger.info("Request 'Checks'") - file_path = "logs/eccs2checks_2020-02-22.log" + file_path = "logs/eccs2checks_%s.log" % eccs2properties.day date = PurePath(file_path).parts[-1].split('_')[1].split('.')[0] pretty = 0 status = None @@ -142,7 +143,6 @@ class Checks(Resource): # Build Email Addresses Link for ECCS2 Web Gui def buildEmailAddress(listContacts): - listCtcs = listContacts.split(",") hrefList = [] @@ -155,7 +155,7 @@ class EccsResults(Resource): def get(self): app.logger.info("Request 'EccsResults'") - file_path = "logs/eccs2_2020-03-01.log" + file_path = "logs/eccs2_%s.log" % eccs2properties.day date = PurePath(file_path).parts[-1].split('_')[1].split('.')[0] pretty = 0 status = None @@ -305,10 +305,15 @@ class EccsResults(Resource): else: return jsonify(result) +# Run check for a specific IDP +# <idpdisc:DiscoveryResponse Location>?entityID=<IDP_ENITIYID>&target=<DESTINATION_RESOURCE_URL> (tutto url encoded) +#class RunCheck(Resource): +# def get(self): api.add_resource(Test, '/eccs/test') # Route_1 api.add_resource(Checks, '/eccs/checks') # Route_2 api.add_resource(EccsResults, '/eccs/eccsresults') # Route_3 +#api.add_resource(RunCheck, '/eccs/runcheck') # Route_4 if __name__ == '__main__': diff --git a/eccs2.py b/eccs2.py index 150211788cdf98aa0a908f4e9bf590917e32f851..cca7f7858f97af9a888848066b2cec9f70c3ddb9 100755 --- a/eccs2.py +++ b/eccs2.py @@ -1,7 +1,23 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3.8 -from datetime import date +import argparse +import json import logging +import time +import os +import eccs2properties +import psutil +import signal +import re +import requests + +from datetime import date +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import Select +from selenium.webdriver.common.keys import Keys +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import TimeoutException """ @@ -31,47 +47,61 @@ def getIdpListFromUrl(): def getIdpListFromFile(): import json - with open('list_eccs_idps-idem.txt','r',encoding='utf-8') as f: + #with open('list_eccs_idps-idem.txt','r',encoding='utf-8') as f: + with open('federation_idps.txt','r',encoding='utf-8') as f: json_data = json.loads(f.read()) return json_data def checkIdP(sp,idp,logger): - from selenium import webdriver - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import Select - from selenium.webdriver.common.keys import Keys - from selenium.common.exceptions import NoSuchElementException - from selenium.common.exceptions import TimeoutException - import re + # Disable SSL requests warning messages + requests.packages.urllib3.disable_warnings() # Configure Web-driver chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless') chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument('--disable-dev-shm-usage') + chrome_options.add_argument('--ignore-certificate-errors') -# driver = webdriver.Chrome('chromedriver', chrome_options=chrome_options, service_args=['--verbose', '--log-path=./selenium_chromedriver.log']) - driver = webdriver.Chrome('chromedriver', chrome_options=chrome_options) + driver = webdriver.Chrome('chromedriver', options=chrome_options, service_args=['--log-path=./selenium_chromedriver.log']) + #driver = webdriver.Chrome('chromedriver', chrome_options=chrome_options, service_args=['--verbose', '--log-path=./selenium_chromedriver.log']) + #driver = webdriver.Chrome('chromedriver', chrome_options=chrome_options) - # Configure timeouts: 45 sec - driver.set_page_load_timeout(45) - driver.set_script_timeout(45) + # Configure timeouts: 30 sec + driver.set_page_load_timeout(30) + driver.set_script_timeout(30) # Configure Blacklists - federation_blacklist = ['http://www.surfconext.nl/','https://www.wayf.dk','http://feide.no/'] - entities_blacklist = ['https://idp.eie.gr/idp/shibboleth','https://gn-vho.grnet.gr/idp/shibboleth','https://wtc.tu-chemnitz.de/shibboleth','https://wtc.tu-chemnitz.de/shibboleth','https://idp.fraunhofer.de/idp/shibboleth','https://login.hs-owl.de/nidp/saml2/metadata','https://idp.dfn-cert.de/idp/shibboleth'] + federation_blacklist = [ + 'http://www.surfconext.nl/', + 'https://www.wayf.dk', + 'http://feide.no/' + ] + + entities_blacklist = [ + 'https://idp.eie.gr/idp/shibboleth', + 'https://gn-vho.grnet.gr/idp/shibboleth', + 'https://wtc.tu-chemnitz.de/shibboleth', + 'https://wtc.tu-chemnitz.de/shibboleth', + 'https://idp.fraunhofer.de/idp/shibboleth', + 'https://login.hs-owl.de/nidp/saml2/metadata', + 'https://idp.dfn-cert.de/idp/shibboleth' + ] - if (idp['entityID'] in entities_blacklist): - logger.info("%s;%s;IdP excluded from checks" % (idp['entityID'],sp)) + if (idp['registrationAuthority'] in federation_blacklist): + logger.info("%s;%s;NULL;Federation excluded from checks" % (idp['entityID'],sp)) driver.close() driver.quit() return "DISABLED" - if (idp['registrationAuthority'] in federation_blacklist): - logger.info("%s;%s;Federation excluded from checks" % (idp['entityID'],sp)) + + if (idp['entityID'] in entities_blacklist): + logger.info("%s;%s;NULL;IdP excluded from checks" % (idp['entityID'],sp)) driver.close() driver.quit() return "DISABLED" + # Open SP, select the IDP from the EDS and press 'Enter' to reach the IdP login page to check try: driver.get(sp) @@ -79,14 +109,17 @@ def checkIdP(sp,idp,logger): driver.find_element_by_id("username") driver.find_element_by_id("password") - - except NoSuchElementException as e: - pass except TimeoutException as e: - logger.info("%s;%s;TIMEOUT" % (idp['entityID'],sp)) + driver.delete_all_cookies() + print("TIMEOUT - driver.current_url: %s" % (driver.current_url)) + status_code = requests.get(driver.current_url, verify=False).status_code + logger.info("%s;%s;%s;TIMEOUT" % (idp['entityID'],sp,status_code)) driver.close() driver.quit() return "TIMEOUT" + except NoSuchElementException as e: + driver.delete_all_cookies() + pass pattern_metadata = "Unable.to.locate(\sissuer.in|).metadata(\sfor|)|no.metadata.found|profile.is.not.configured.for.relying.party|Cannot.locate.entity|fail.to.load.unknown.provider|does.not.recognise.the.service|unable.to.load.provider|Nous.n'avons.pas.pu.(charg|charger).le.fournisseur.de service|Metadata.not.found|application.you.have.accessed.is.not.registered.for.use.with.this.service|Message.did.not.meet.security.requirements" @@ -98,17 +131,26 @@ def checkIdP(sp,idp,logger): password_found = re.search(pattern_password,driver.page_source, re.I) if(metadata_not_found): - logger.info("%s;%s;No-eduGAIN-Metadata" % (idp['entityID'],sp)) + #print("MD-NOT-FOUND - driver.current_url: %s" % (driver.current_url)) + status_code = requests.get(driver.current_url, verify=False).status_code + logger.info("%s;%s;%s;No-eduGAIN-Metadata" % (idp['entityID'],sp,status_code)) + driver.delete_all_cookies() driver.close() driver.quit() return "No-eduGAIN-Metadata" - elif not username_found and not password_found: - logger.info("%s;%s;Invalid-Form" % (idp['entityID'],sp)) + elif not username_found or not password_found: + #print("INVALID-FORM - entityID: %s, sp: %s, driver.current_url: %s" % (idp['entityID'],sp,driver.current_url)) + status_code = requests.get(driver.current_url, verify=False).status_code + logger.info("%s;%s;%s;Invalid-Form" % (idp['entityID'],sp,status_code)) + driver.delete_all_cookies() driver.close() driver.quit() return "Invalid Form" else: - logger.info("%s;%s;OK" % (idp['entityID'],sp)) + #print("MD-FOUND - driver.current_url: %s" % (driver.current_url)) + status_code = requests.get(driver.current_url, verify=False).status_code + logger.info("%s;%s;%s;OK" % (idp['entityID'],sp,status_code)) + driver.delete_all_cookies() driver.close() driver.quit() return "OK" @@ -118,7 +160,7 @@ def checkIdP(sp,idp,logger): def getLogger(filename,log_level="DEBUG",path="./"): logger = logging.getLogger(filename) - ch = logging.FileHandler(path+filename,'w','utf-8') + ch = logging.FileHandler(path+filename,'a','utf-8') if (log_level == "DEBUG"): logger.setLevel(logging.DEBUG) @@ -154,23 +196,11 @@ def getIdPContacts(idp,contactType): return ctcList -# MAIN -if __name__=="__main__": - - day = date.today().isoformat() - - eccs2log = getLogger("logs/eccs2_"+day+".log","INFO") - eccs2checksLog = getLogger("logs/eccs2checks_"+day+".log","INFO") - - sps = ["https://sp24-test.garr.it/secure", "https://attribute-viewer.aai.switch.ch/eds/"] - - #listIdPs = getIdpListFromUrl() - listIdPs = getIdpListFromFile() - - for idp in listIdPs: +def checkIdp(idp,sps,eccs2log,eccs2checksLog): result = [] for sp in sps: - result.append(checkIdP(sp,idp,eccs2checksLog)) + resultCheck = checkIdP(sp,idp,eccs2checksLog) + result.append(resultCheck) listTechContacts = getIdPContacts(idp,'technical') listSuppContacts = getIdPContacts(idp,'support') @@ -216,3 +246,21 @@ if __name__=="__main__": result[0], sps[1], result[1])) + +# MAIN +if __name__=="__main__": + + eccs2log = getLogger("logs/"+eccs2properties.ECCS2LOGPATH,"INFO") + eccs2checksLog = getLogger("logs/"+eccs2properties.ECCS2CHECKSLOGPATH,"INFO") + + sps = ["https://sp24-test.garr.it/secure", "https://attribute-viewer.aai.switch.ch/eds/"] + #sps = ["https://attribute-viewer.aai.switch.ch/eds/", "https://attribute-viewer.aai.switch.ch/eds/"] + + parser = argparse.ArgumentParser(description='Checks if the input IdP consumed correctly eduGAIN metadata by accessing two different SPs') + parser.add_argument("idpJson", metavar="idpJson", nargs=1, help="An IdP in Json format") + + args = parser.parse_args() + + idp = json.loads(args.idpJson[0]) + + checkIdp(idp,sps,eccs2log,eccs2checksLog) diff --git a/runEccs2.py b/runEccs2.py new file mode 100755 index 0000000000000000000000000000000000000000..664f7dbc481dce29e68af6f81a46efd121764ebc --- /dev/null +++ b/runEccs2.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3.8 + +import asyncio +import eccs2properties +import json +import sys +import time + +from subprocess import Popen,PIPE + +def getIdPs(): + import certifi + import urllib3 + import json + + manager = urllib3.PoolManager( + cert_reqs='CERT_REQUIRED', + ca_certs=certifi.where() + ) + + url = "https://technical.edugain.org/api.php?action=list_eccs_idps" + idp_json = manager.request('GET', url) + + idp_dict = json.loads(idp_json.data.decode('utf-8')) + + idp_list = [] + + #federation = input("Insert the registrationAuthority: ") + federation = "http://www.idem.garr.it/" + + for idp in idp_dict: + if (idp['registrationAuthority'] == federation): + idp_list.append(idp) + + return json.dumps(idp_list) + + +def getIdpListFromFile(): + import json + + #with open('list_eccs_idps-idem.txt','r',encoding='utf-8') as f: + with open('federation_idps.txt','r',encoding='utf-8') as f: + json_data = json.loads(f.read()) + return json_data + + +async def run(name,queue,stdout_file,stderr_file): + while True: + # Get a "cmd item" out of the queue. + cmd = await queue.get() + + # Elaborate "cmd" from shell. + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await proc.communicate() + + if stdout: + stdout_file.write(f'[stdout]\n{stdout.decode()}') + if stderr: + stderr_file.write(f'[stderr]\n{stderr.decode()}\n\n[cmd]\n{cmd}') + + # Notify the queue that the "work cmd" has been processed. + queue.task_done() + + +async def main(cmd_list,stdout_file,stderr_file): + # Create a queue that we will use to store our "workload". + queue = asyncio.Queue() + + # Put all commands into the queue. + for cmd in cmd_list: + queue.put_nowait(cmd) + + # Create worker tasks to process the queue concurrently. + tasks = [] + for i in range(30): + task = asyncio.create_task(run("cmd-{%d}" % i, queue, stdout_file, stderr_file)) + tasks.append(task) + + # Wait until the queue is fully processed. + started_at = time.monotonic() + await queue.join() + total_slept_for = time.monotonic() - started_at + + # Cancel our worker tasks. + for task in tasks: + task.cancel() + + # Wait until all worker tasks are cancelled. + await asyncio.gather(*tasks, return_exceptions=True) + + +# MAIN +if __name__=="__main__": + + start = time.time() + + ''' + data = getIdPs() + + f = open('federation_idps.txt', 'w') + f.write(data) + f.close() + ''' + stdout_file = open(eccs2properties.ECCS2STDOUT,"w+") + stderr_file = open(eccs2properties.ECCS2STDERR,"w+") + + idpJsonList = getIdpListFromFile() + num_idps = len(idpJsonList) + cmd_list = [["%s/eccs2.py \'%s\'" % (eccs2properties.ECCS2PATH, json.dumps(idp))] for idp in idpJsonList] + + proc_list = [] + count = 0 + while (count < num_idps): + cmd = "".join(cmd_list.pop()) + proc_list.append(cmd) + count = count + 1 + + asyncio.run(main(proc_list,stdout_file,stderr_file)) + + end = time.time() + print("Time taken in seconds - ", end - start) diff --git a/web/index.php b/web/index.php index b1de390cc920a1e525cf3ecfe17092ad43b3ed13..8bead0e5d81cf2869df594c5a9a3d558ef83b639 100644 --- a/web/index.php +++ b/web/index.php @@ -2,7 +2,7 @@ <html> <head> - <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script> + <script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.js"></script> <script type="text/javascript" src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css"/> @@ -14,7 +14,7 @@ </head> <body> <div class="container"> - <table id="example" class="display" style="width:100%"> + <table id="eccstable" class="display" style="width:100%"> <thead> <tr> <th></th> @@ -22,8 +22,7 @@ <th>EntityID</th> <th>Registration Authority</th> <th>Technical Contacts</th> - <th>Support Contacts</th> - <th>Date</th> + <th>Check Date</th> <th>Status</th> </tr> </thead> diff --git a/web/script.js b/web/script.js index ba653f25b3aee1291e3e60b9eed5bdffb58b3170..300d347742e42140665e75664d2c25033d11c679 100644 --- a/web/script.js +++ b/web/script.js @@ -12,6 +12,11 @@ function format ( d ) { '<td>'+d.contacts.technical+'</td>'+ '<td></td>'+ '</tr>'+ + '<tr>'+ + '<td>Support Contacts:</td>'+ + '<td>'+d.contacts.support+'</td>'+ + '<td></td>'+ + '</tr>'+ '<tr>'+ '<td>SP1:</td>'+ '<td>'+d.sp1.entityID+'</td>'+ @@ -26,7 +31,7 @@ function format ( d ) { } $(document).ready(function() { - var table = $('#example').DataTable( { + var table = $('#eccstable').DataTable( { "ajax": { "url": "data.json", "dataSrc": "" @@ -46,11 +51,7 @@ $(document).ready(function() { { "data": "registrationAuthority" }, { "data": "contacts.technical", - "defaultContent": '' - }, - { - "data": "contacts.support", - "defaultContent": '' + "defaultContent": "" }, { "data": "date" }, { "data": "status" } @@ -70,7 +71,7 @@ $(document).ready(function() { } ); // Add event listener for opening and closing details - $('#example tbody').on('click', 'td.details-control', function () { + $('#eccstable tbody').on('click', 'td.details-control', function () { var tr = $(this).closest('tr'); var row = table.row( tr );