diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 786aa7b..4c0876b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = "0.11" +reqwest = { version = "0.11", features = ["cookies"] } axum = { version = "0.7.4" } tower = { version = "0.4", features = ["util"] } tower-http = { version = "0.5.1", features = ["fs", "trace"] } diff --git a/backend/src/course_fetcher.rs b/backend/src/course_fetcher.rs index 99ea4fa..4ae9593 100644 --- a/backend/src/course_fetcher.rs +++ b/backend/src/course_fetcher.rs @@ -15,8 +15,6 @@ pub type CourseMap = RwLock>>; lazy_static! { static ref COURSE_REGEX: Regex = Regex::new(r#""#).unwrap(); - static ref SEMESTER_PARENT_REGEX: Regex = Regex::new(r#"\[()\]|,\[(\[.*?\])\]"#).unwrap(); - static ref SEMESTER_LITERAL_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap(); } #[derive(Serialize)] @@ -28,11 +26,12 @@ pub struct Semester { pub struct CourseFetcher { pub course: CourseMap, // Course, Semester + pub client: reqwest::Client, } impl CourseFetcher { async fn fetch(&self) -> anyhow::Result<()> { - let res = reqwest::get(STUNDENPLAN_URL).await?; + let res = crate::moodle_client::get_moodle(&self.client, STUNDENPLAN_URL).await?; if !res.status().is_success() { return Err(anyhow!("status code was {}", &res.status().as_str())); @@ -119,23 +118,31 @@ fn parse_courses(body: &str) -> anyhow::Result> { Ok(courses) } -fn parse_semester(body: &str) -> anyhow::Result>> { - let mut result = Vec::new(); +fn parse_semester(body: &str) -> anyhow::Result>> { + lazy_static! { + static ref KURSE_JSON_REGEX: Regex = Regex::new(r#"var kurse\s*=\s*(\[[\s\S]*?\]);"#).unwrap(); + } - for capture in SEMESTER_PARENT_REGEX.captures_iter(body) { - let matched = capture.get(0).unwrap(); + let caps = KURSE_JSON_REGEX.captures(body) + .ok_or_else(|| anyhow!("could not find var kurse in body"))?; - let mut child_vec = Vec::new(); + let json_str = caps.get(1).unwrap().as_str(); + let parsed: Vec>> = serde_json::from_str(json_str)?; - for child in SEMESTER_LITERAL_REGEX.captures_iter(matched.as_str()) { - let mut course = child.get(1).ok_or_else(|| anyhow!("no string in match found"))?.as_str(); - if course.contains(".") { - // strip stuff like .html - course = course.split(".").collect::>()[0]; + let mut result = Vec::new(); + for course_semesters in parsed { + let mut child_vec = Vec::new(); + for sem in course_semesters { + if let Some(mut course) = sem.into_iter().next() { + if course.contains(".") { + // strip stuff like .html + if let Some(first_part) = course.split(".").next() { + course = first_part.to_string(); + } + } + child_vec.push(course); } - child_vec.push(course); } - result.push(child_vec); } diff --git a/backend/src/main.rs b/backend/src/main.rs index b8e801b..1bebc96 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -20,12 +20,14 @@ use crate::course_fetcher::CourseFetcher; mod routes; mod consts; mod course_fetcher; +mod moodle_client; pub struct AppError(anyhow::Error); #[derive(Clone)] pub struct AppState { pub course_fetcher: Arc, + pub client: reqwest::Client, } #[tokio::main] @@ -38,10 +40,16 @@ async fn main() -> Result<(), Box> { let port = std::env::var("PORT").unwrap().parse::().unwrap(); + let client = reqwest::Client::builder() + .cookie_store(true) + .build()?; + let app_state = AppState { course_fetcher: Arc::new(CourseFetcher { - course: Default::default() - }) + course: Default::default(), + client: client.clone(), + }), + client, }; course_fetcher::start(app_state.course_fetcher.clone()); diff --git a/backend/src/moodle_client.rs b/backend/src/moodle_client.rs new file mode 100644 index 0000000..b270aa9 --- /dev/null +++ b/backend/src/moodle_client.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; +use anyhow::anyhow; +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref TOKEN_REGEX: Regex = Regex::new(r#"name="logintoken"\s+value="([^"]+)""#).unwrap(); +} + +pub async fn login_as_guest(client: &reqwest::Client) -> anyhow::Result<()> { + let login_url = "https://moodle.hwr-berlin.de/login/index.php"; + let res = client.get(login_url).send().await?; + let body = res.text().await?; + + let token = TOKEN_REGEX.captures(&body) + .and_then(|c| c.get(1)) + .map(|m| m.as_str()) + .ok_or_else(|| anyhow!("could not find logintoken in Moodle login page"))?; + + let mut params = HashMap::new(); + params.insert("logintoken", token); + params.insert("username", "guest"); + params.insert("password", "guest"); + + let res = client.post(login_url) + .form(¶ms) + .send() + .await?; + + if !res.status().is_success() { + return Err(anyhow!("Moodle guest login POST failed: status {}", res.status())); + } + + Ok(()) +} + +pub async fn get_moodle(client: &reqwest::Client, url: &str) -> anyhow::Result { + let res = client.get(url).send().await?; + + if res.url().path().contains("/login/index.php") { + tracing::info!("Redirected to login page. Performing guest login..."); + login_as_guest(client).await?; + + tracing::info!("Retrying request to {} after guest login", url); + let res = client.get(url).send().await?; + if res.url().path().contains("/login/index.php") { + return Err(anyhow!("Redirected to login page even after guest login")); + } + Ok(res) + } else { + Ok(res) + } +} diff --git a/backend/src/routes/ical.rs b/backend/src/routes/ical.rs index 752e0c8..6567c05 100644 --- a/backend/src/routes/ical.rs +++ b/backend/src/routes/ical.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use anyhow::anyhow; use axum::body::Body; use axum::extract::{Path, Query}; +use axum::Extension; use axum::http::StatusCode; use axum::response::Response; use encoding_rs::UTF_8; @@ -12,12 +13,13 @@ use regex::{Regex, RegexBuilder}; use crate::AppError; use crate::consts::{MAX_REGEX_COUNT, REVERSE_PROXY_URL}; -pub async fn root_without_regex(Path((study, semester, course)): Path<(String, String, String)>, query: Query>) -> Result, AppError> { - root(Path((study, semester, course, None)), query).await +pub async fn root_without_regex(Extension(state): Extension, Path((study, semester, course)): Path<(String, String, String)>, query: Query>) -> Result, AppError> { + root(Extension(state), Path((study, semester, course, None)), query).await } -pub async fn root(Path((study, semester, course, regex)): Path<(String, String, String, Option)>, Query(query): Query>) -> Result, AppError> { - let res = reqwest::get(REVERSE_PROXY_URL.to_owned() + &format!("{study}/{semester}/{course}")).await.unwrap(); +pub async fn root(Extension(state): Extension, Path((study, semester, course, regex)): Path<(String, String, String, Option)>, Query(query): Query>) -> Result, AppError> { + let url = REVERSE_PROXY_URL.to_owned() + &format!("{study}/{semester}/{course}"); + let res = crate::moodle_client::get_moodle(&state.client, &url).await.map_err(|e| anyhow!(e))?; let status = res.status(); let body = res.bytes().await.unwrap(); let res = UTF_8.decode_with_bom_removal(&body).0.to_string(); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 01d7552..cf1006c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -87,7 +87,8 @@ const resultUrl = ref("-- Ergebnis --"); const resultElement = ref(); async function generate() { - let url = `${API_URL}/${selectedCourse.value.toLowerCase()}/${selectedSemester.value!.year_part.toLowerCase()}/${selectedSemester.value!.course_part}`; + const base = API_URL || window.location.origin; + let url = `${base}/${selectedCourse.value.toLowerCase()}/${selectedSemester.value!.year_part.toLowerCase()}/${selectedSemester.value!.course_part}`; for (let filter of filterItems.value) { if (filter.value !== "")