Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
37 changes: 22 additions & 15 deletions backend/src/course_fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ pub type CourseMap = RwLock<HashMap<String, Vec<Semester>>>;

lazy_static! {
static ref COURSE_REGEX: Regex = Regex::new(r#"<option value=".*?"[^>]*?>(.*?)</option>"#).unwrap();
static ref SEMESTER_PARENT_REGEX: Regex = Regex::new(r#"\[()\]|,\[(\[.*?\])\]"#).unwrap();
static ref SEMESTER_LITERAL_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap();
}

#[derive(Serialize)]
Expand All @@ -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()));
Expand Down Expand Up @@ -119,23 +118,31 @@ fn parse_courses(body: &str) -> anyhow::Result<Vec<&str>> {
Ok(courses)
}

fn parse_semester(body: &str) -> anyhow::Result<Vec<Vec<&str>>> {
let mut result = Vec::new();
fn parse_semester(body: &str) -> anyhow::Result<Vec<Vec<String>>> {
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<Vec<Vec<String>>> = 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::<Vec<&str>>()[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);
}

Expand Down
12 changes: 10 additions & 2 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CourseFetcher>,
pub client: reqwest::Client,
}

#[tokio::main]
Expand All @@ -38,10 +40,16 @@ async fn main() -> Result<(), Box<dyn Error>> {

let port = std::env::var("PORT").unwrap().parse::<u16>().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());
Expand Down
53 changes: 53 additions & 0 deletions backend/src/moodle_client.rs
Original file line number Diff line number Diff line change
@@ -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(&params)
.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<reqwest::Response> {
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)
}
}
10 changes: 6 additions & 4 deletions backend/src/routes/ical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<HashMap<String, String>>) -> Result<Response<Body>, AppError> {
root(Path((study, semester, course, None)), query).await
pub async fn root_without_regex(Extension(state): Extension<crate::AppState>, Path((study, semester, course)): Path<(String, String, String)>, query: Query<HashMap<String, String>>) -> Result<Response<Body>, 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<String>)>, Query(query): Query<HashMap<String, String>>) -> Result<Response<Body>, AppError> {
let res = reqwest::get(REVERSE_PROXY_URL.to_owned() + &format!("{study}/{semester}/{course}")).await.unwrap();
pub async fn root(Extension(state): Extension<crate::AppState>, Path((study, semester, course, regex)): Path<(String, String, String, Option<String>)>, Query(query): Query<HashMap<String, String>>) -> Result<Response<Body>, 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();
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ const resultUrl = ref<string>("-- Ergebnis --");
const resultElement = ref<HTMLParagraphElement>();

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 !== "")
Expand Down