pub mod course_modules;
pub mod user;
pub mod course;
pub mod news;
pub mod questionnaire;
pub mod ref_source;
pub mod institute;
use std::cell::RefCell;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context};
use reqwest::blocking::{Client, ClientBuilder};
use reqwest::header::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::course::MyCourses;
const LOGIN_URL : &str = "https://studip.example.com/Shibboleth.sso/Login";
const SAML_RESPONSE_URL: &str = "https://studip.example.com/Shibboleth.sso/SAML2/POST";
const REQUEST_MAX_SPEED: Duration = Duration::from_millis(150);
pub struct StudIp {
pub client: Arc<StudIpClient>,
pub my_courses: MyCourses
}
impl StudIp {
fn login_client<IdP: IdentityProvider>(&self, creds_path: &str) -> anyhow::Result<()> {
let _ = self.client.get("https://studip.example.com/index.php?logout=true&set_language=de_DE&set_contrast=").send();
let creds = std::fs::read_to_string(creds_path)
.context("Could not read from creds.txt")?;
let (username, password) = creds.split_once('\n')
.context("creds.txt did not have newline seperated username and password")?;
let mut target_url = Url::parse(&format!("https://{}", self.client.host))?;
target_url.query_pairs_mut()
.append_pair("sso", "shib")
.append_pair("again", "yes")
.append_pair("cancel_login", "1");
let redirected_url = self.client.get(LOGIN_URL)
.query(&[
("target", target_url.as_str()),
("entityID", IdP::entity_url())
])
.send()?
.url()
.clone();
let saml_assertion = IdP::login(&self.client.client, redirected_url, username, password)?;
let response = self.client.post(SAML_RESPONSE_URL)
.form(&[("RelayState", saml_assertion.relay_state), ("SAMLResponse", saml_assertion.saml_response)])
.send()
.context("Could not send second login request. Are the credentials incorrect?")?;
if !response.status().is_success() {
bail!("Second login request had status code: {}", response.status());
}
Ok(())
}
fn make_client() -> anyhow::Result<Client> {
let mut default_headers = HeaderMap::new();
default_headers.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0"));
default_headers.insert("Accept", HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"));
default_headers.insert("Accept-Language", HeaderValue::from_static("en-US,en;q=0.5"));
default_headers.insert("Upgrade-Insecure-Requests", HeaderValue::from_static("1"));
default_headers.insert("DNT", HeaderValue::from_static("1"));
default_headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("document"));
default_headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("navigate"));
default_headers.insert("Sec-Fetch-Site", HeaderValue::from_static("cross-site"));
ClientBuilder::new()
.https_only(true)
.cookie_store(true)
.timeout(Duration::from_secs(8))
.default_headers(default_headers)
.gzip(true)
.build()
.context("Could not build reqwest client")
}
pub fn login<IdP: IdentityProvider>(creds_path: &str, host: &'static str) -> anyhow::Result<Self> {
let client = Arc::new(
StudIpClient {
client: Self::make_client()?,
host,
#[cfg(feature = "rate_limiting")]
last_request_time: RefCell::new(SystemTime::UNIX_EPOCH),
}
);
let stud_ip = Self {
client: client.clone(),
my_courses: MyCourses::from_client(client),
};
stud_ip.login_client::<IdP>(creds_path)?;
Ok(stud_ip)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SAMLAssertionData {
pub relay_state: String,
pub saml_response: String,
}
pub trait IdentityProvider {
fn login(client: &Client, url: impl reqwest::IntoUrl, username: &str, password: &str) -> anyhow::Result<SAMLAssertionData>;
fn entity_url() -> &'static str;
}
#[derive(Debug)]
pub struct StudIpClient {
pub client: Client,
pub host: &'static str,
#[cfg(feature = "rate_limiting")]
last_request_time: RefCell<SystemTime>,
}
impl Default for StudIpClient {
fn default() -> Self {
Self {
client: Default::default(),
host: "",
#[cfg(feature = "rate_limiting")]
last_request_time: RefCell::new(SystemTime::UNIX_EPOCH),
}
}
}
impl StudIpClient {
#[cfg(feature = "rate_limiting")]
fn before_request(&self) {
let mut last_request_time = self.last_request_time.borrow_mut();
let elapsed = last_request_time.elapsed().unwrap_or(Duration::from_secs(0));
if elapsed > REQUEST_MAX_SPEED {
*last_request_time = SystemTime::now();
return;
}
let wait_time = REQUEST_MAX_SPEED - elapsed;
std::thread::sleep(wait_time);
*last_request_time = SystemTime::now();
}
#[cfg(not(feature = "rate_limiting"))]
fn before_request(&self) {}
}
macro_rules! impl_client_wrap {
($($method:ident),+) => {
impl StudIpClient {
$(
pub fn $method(&self, url: impl reqwest::IntoUrl) -> reqwest::blocking::RequestBuilder {
self.before_request();
let mut url : Url = url.into_url().unwrap();
url.set_host(Some(self.host)).unwrap();
#[cfg(feature = "verbose")]
{
println!("{}: {}", stringify!($method), url.as_str());
}
self.client.$method(url)
}
)+
}
};
}
impl_client_wrap!(get, post, put, patch, delete, head);