eClass MCP Server
by sdi2200262
Verified
#!/usr/bin/env python3
"""
eClass Client - Authentication and Basic Functionality
This module provides a client for interacting with an eClass platform instance.
It handles authentication and session management for accessing eClass resources.
Specifically tailored for UoA's SSO authentication system.
"""
import os
import re
import logging
import requests
from bs4 import BeautifulSoup
from dotenv import load_dotenv
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('eclass_client')
class EClassClient:
"""Client for interacting with an eClass platform instance through UoA's SSO."""
def __init__(self, base_url=None):
"""
Initialize the eClass client.
Args:
base_url (str, optional): The base URL of the eClass instance.
If not provided, it will be loaded from the environment.
"""
# Load environment variables
load_dotenv()
# Initialize session and state
self.session = requests.Session()
self.logged_in = False
# Set base URL
self.base_url = base_url or os.getenv('ECLASS_URL')
if not self.base_url:
raise ValueError("eClass URL not provided and not found in environment")
# Remove trailing slash if present
self.base_url = self.base_url.rstrip('/')
# Set SSO URLs
self.login_form_url = f"{self.base_url}/main/login_form.php"
logger.info(f"Initialized eClass client for {self.base_url}")
def login(self, username=None, password=None):
"""
Log in to eClass using username/password through UoA's SSO.
Args:
username (str, optional): eClass username.
If not provided, it will be loaded from the environment.
password (str, optional): eClass password.
If not provided, it will be loaded from the environment.
Returns:
bool: True if login was successful, False otherwise.
"""
username = username or os.getenv('ECLASS_USERNAME')
password = password or os.getenv('ECLASS_PASSWORD')
if not username or not password:
raise ValueError("Username and password must be provided or set in environment")
logger.info(f"Attempting to log in as {username}")
# Step 1: Visit the eClass login form page
try:
response = self.session.get(self.login_form_url)
response.raise_for_status()
logger.info("Accessed eClass login form")
except requests.RequestException as e:
logger.error(f"Failed to access login form: {e}")
return False
# Step 2: Find the SSO login button and follow it
try:
soup = BeautifulSoup(response.text, 'html.parser')
sso_link = None
# Look for the UoA login button (it could be a button or a link)
for link in soup.find_all('a'):
if 'Είσοδος με λογαριασμό ΕΚΠΑ' in link.text or 'ΕΚΠΑ' in link.text:
sso_link = link.get('href')
break
if not sso_link:
# Try alternate method to find the SSO link
for form in soup.find_all('form'):
if form.get('action') and 'cas.php' in form.get('action'):
sso_link = form.get('action')
break
if not sso_link:
logger.error("Could not find SSO login link on the login page")
return False
# Make sure the URL is absolute
if not sso_link.startswith(('http://', 'https://')):
if sso_link.startswith('/'):
sso_link = f"{self.base_url}{sso_link}"
else:
sso_link = f"{self.base_url}/{sso_link}"
logger.info(f"Found SSO login link: {sso_link}")
# Follow the SSO link
response = self.session.get(sso_link)
response.raise_for_status()
logger.info("Redirected to SSO login page")
except requests.RequestException as e:
logger.error(f"Failed to access SSO page: {e}")
return False
except Exception as e:
logger.error(f"Error parsing login page: {e}")
return False
# Step 3: Extract execution parameter and submit login form to CAS
try:
soup = BeautifulSoup(response.text, 'html.parser')
# Check if we're already on the CAS login page
if 'sso.uoa.gr' not in response.url:
logger.error(f"Unexpected redirect to {response.url}")
return False
# Find execution value
execution_input = soup.find('input', {'name': 'execution'})
if not execution_input:
logger.error("Could not find execution parameter on SSO page")
return False
execution = execution_input.get('value')
# Find login form action
form = soup.find('form', {'id': 'fm1'}) or soup.find('form')
if not form:
logger.error("Could not find login form on SSO page")
return False
action = form.get('action')
if not action:
# Use the current URL as fallback
action = response.url
elif not action.startswith(('http://', 'https://')):
# Make the action URL absolute
if action.startswith('/'):
action = f"https://sso.uoa.gr{action}"
else:
action = f"https://sso.uoa.gr/{action}"
# Prepare login data
login_data = {
'username': username,
'password': password,
'execution': execution,
'_eventId': 'submit',
'geolocation': ''
}
# Submit login form
response = self.session.post(action, data=login_data)
response.raise_for_status()
# Check for authentication errors
if 'Πόροι Πληροφορικής ΕΚΠΑ' not in response.text and 'The credentials you provided cannot be determined to be authentic' not in response.text:
logger.info("Successfully authenticated with SSO")
else:
soup = BeautifulSoup(response.text, 'html.parser')
error_msg = soup.find('div', {'id': 'msg'})
if error_msg:
logger.error(f"SSO login error: {error_msg.text.strip()}")
else:
logger.error("SSO login failed without specific error message")
return False
except requests.RequestException as e:
logger.error(f"Failed during SSO authentication: {e}")
return False
except Exception as e:
logger.error(f"Error during SSO authentication: {e}")
return False
# Step 4: Check if we've been redirected to eClass and verify login success
try:
# We should now be redirected back to eClass
if 'eclass.uoa.gr' in response.url:
# Try to access portfolio page to verify login
portfolio_url = f"{self.base_url}/main/portfolio.php"
response = self.session.get(portfolio_url)
response.raise_for_status()
# Check if we can access the portfolio page
if 'Μαθήματα' in response.text or 'portfolio' in response.text.lower() or 'course' in response.text.lower():
self.logged_in = True
logger.info("Login successful, redirected to eClass portfolio")
return True
else:
logger.error("Could not access portfolio page after login")
return False
else:
logger.error(f"Unexpected redirect after login: {response.url}")
return False
except requests.RequestException as e:
logger.error(f"Failed to verify login: {e}")
return False
except Exception as e:
logger.error(f"Error verifying login: {e}")
return False
def get_courses(self):
"""
Get list of enrolled courses.
Returns:
list: A list of dictionaries containing course information.
"""
if not self.logged_in:
logger.error("Not logged in")
return []
courses_url = f"{self.base_url}/main/portfolio.php"
try:
response = self.session.get(courses_url)
response.raise_for_status()
except requests.RequestException as e:
logger.error(f"Failed to fetch courses: {e}")
return []
courses = []
try:
soup = BeautifulSoup(response.text, 'html.parser')
# Try different CSS selectors that might contain course information
course_elements = (
soup.select('.course-title') or
soup.select('.lesson-title') or
soup.select('.course-box .title') or
soup.select('.course-info h4')
)
if not course_elements:
# Try to find course links using a more general approach
for link in soup.find_all('a'):
href = link.get('href', '')
if 'courses' in href or 'course.php' in href:
course_name = link.text.strip()
if course_name: # Only include if there's a name
courses.append({
'name': course_name,
'url': href if href.startswith('http') else f"{self.base_url}/{href.lstrip('/')}"
})
else:
for course_elem in course_elements:
course_link = course_elem.find('a') or course_elem
if course_link and course_link.get('href'):
course_name = course_link.text.strip()
course_url = course_link.get('href')
courses.append({
'name': course_name,
'url': course_url if course_url.startswith('http') else f"{self.base_url}/{course_url.lstrip('/')}"
})
except Exception as e:
logger.error(f"Failed to parse courses: {e}")
logger.info(f"Found {len(courses)} courses")
return courses
def logout(self):
"""
Log out from eClass.
Returns:
bool: True if logout was successful, False otherwise.
"""
if not self.logged_in:
logger.warning("Not logged in, nothing to do")
return True
logout_url = f"{self.base_url}/index.php?logout=yes"
try:
response = self.session.get(logout_url)
response.raise_for_status()
self.logged_in = False
logger.info("Logged out successfully")
return True
except requests.RequestException as e:
logger.error(f"Logout failed: {e}")
return False
def main():
"""Main function to demonstrate eClass client usage."""
try:
# Create client instance
client = EClassClient()
# Attempt to login
if client.login():
print("Login successful!")
# Get courses
courses = client.get_courses()
print(f"Found {len(courses)} courses:")
for i, course in enumerate(courses, 1):
print(f"{i}. {course['name']}")
# Logout
client.logout()
else:
print("Login failed. Please check your credentials.")
except Exception as e:
logger.error(f"An error occurred: {e}")
print(f"Error: {e}")
if __name__ == "__main__":
main()