use actix_web::client::Client; use actix_web::{http, web, HttpRequest, HttpResponse}; use base64::URL_SAFE_NO_PAD; use percent_encoding::percent_decode_str; use rand::rngs::OsRng; use rand::Rng; use rand::RngCore; use regex::Regex; use std::collections::HashMap; use std::time::Duration; use crate::config::{ADJ_LIST, NAME_LIST, PROXY_TIMEOUT, USER_AGENT}; use crate::debug; use crate::errors::{crash, TrainCrash}; use crate::templates::get_lang; use crate::CONFIG; #[derive(Serialize)] struct NCLoginForm<'a> { pub user: &'a str, pub password: &'a str, pub timezone: &'a str, pub timezone_offset: &'a str, pub requesttoken: &'a str, } // check if the user is connected to Nextcloud // returns Some(cookie_raw_value) if connected // returns None if disconnected pub fn is_logged_in(req: &HttpRequest) -> Option<&str> { let c = req.headers().get("Cookie")?.to_str().ok()?; if c.contains("nc_username") { Some(c) } else { None } } // attempts to create the account from Nextcloud's API // returns the newly created username. // if it fails (bad return code), returns None. pub async fn create_account( client: &web::Data, user: &str, password: &str, lang: String, ) -> Result { let mut register_query = client .post(format!( "{}/{}", CONFIG.nextcloud_url, "ocs/v1.php/cloud/users" )) .timeout(Duration::new(PROXY_TIMEOUT, 0)) .basic_auth(&CONFIG.admin_username, Some(&CONFIG.admin_password)) .header( http::header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .header("OCS-APIRequest", "true") .send_form(&NCCreateAccountForm { userid: user, password, quota: "0B", language: &lang, }) .await .map_err(|e| { eprintln!("error_createaccount_post: {}", e); crash(lang.clone(), "error_createaccount_post") })?; // only 200 http status code is allowed if register_query.status() != 200 { eprintln!("error_createaccount_status: {}", register_query.status()); // + extract response body for debugging purposes let response_body = register_query.body().await.map_err(|e| { eprintln!("error_createaccount_post_body: {}", e); crash(lang.clone(), "error_createaccount_post_body") })?; debug(&format!("Body: {:#?}", response_body)); return Err(crash(lang.clone(), "error_createaccount_status")); } // extract response body let response_body = register_query.body().await.map_err(|e| { eprintln!("error_createaccount_post_body: {}", e); crash(lang.clone(), "error_createaccount_post_body") })?; let response_body = String::from_utf8_lossy(&response_body); // grasp NC status code let status_start = response_body.find("").ok_or_else(|| { eprintln!("error_createaccount_ncstatus_parse: start missing"); crash(lang.clone(), "error_createaccount_ncstatus_parse") })? + 12; let status_end = response_body.find("").ok_or_else(|| { eprintln!("error_createaccount_ncstatus_parse: end missing"); crash(lang.clone(), "error_createaccount_ncstatus_parse") })?; let code = &response_body[status_start..status_end]; match code.parse::() { Ok(100) => Ok(String::from(user)), // success Ok(r) => { eprintln!("error_createaccount_ncstatus: {}", r); Err(crash(lang.clone(), "error_createaccount_ncstatus")) } Err(e) => { eprintln!("error_createaccount_ncstatus_parse: {}", e); Err(crash(lang.clone(), "error_createaccount_ncstatus_parse")) } } } #[derive(Serialize)] struct NCCreateAccountForm<'a> { pub userid: &'a str, pub password: &'a str, pub quota: &'a str, pub language: &'a str, } pub async fn login( client: &web::Data, req: &HttpRequest, user: &str, password: &str, ) -> Result { debug(&format!("Sending forged login for user {}", user)); // 1. GET /csrftoken let mut login_get = client .get(format!("{}/{}", CONFIG.nextcloud_url, "csrftoken")) .timeout(Duration::new(PROXY_TIMEOUT, 0)) .header("User-Agent", USER_AGENT) .send() .await .map_err(|e| { eprintln!("error_login_get: {}", e); crash(get_lang(&req), "error_login_get") })?; // rewrite cookie headers from GET to POST let mut str_cookiepair = String::new(); // remove duplicate oc cookie (nextcloud bug) // leading to sncf being unable to forge logins let cookie_set = login_get.headers().get_all("set-cookie"); let mut cookie_map: HashMap = HashMap::new(); for c in cookie_set { // get str version of cookie header let c_str = c.to_str().map_err(|e| { eprintln!("error_login_cookiepair (1): {}", e); crash(get_lang(&req), "error_login_cookiepair") })?; // percent decode let c_str = percent_decode_str(c_str).decode_utf8_lossy(); //then remove values after ';' let c_str_arr = c_str.split(';').collect::>(); let c_str = c_str_arr .first() .expect("error: cookiepair split does not have a first value. shouldn't happen."); // split cookie key and cookie value // split_once would work best but it's nightly-only for now let c_str_arr = c_str.split('=').collect::>(); let c_key = c_str_arr .first() .expect("error: cookie key split does not have a first value, shouldn't happen."); let c_value = c_str.replace(&format!("{}=", c_key), ""); if c_key != c_str { // if the key already exists in hashmap, replace its value // else, insert it if let Some(c_sel) = cookie_map.get_mut(*c_key) { *c_sel = c_value; } else { cookie_map.insert(c_key.to_string(), c_value); } } else { eprintln!("error_login_cookiepair (2)"); return Err(crash(get_lang(&req), "error_login_cookiepair")); } } for (cookie_k, cookie_v) in cookie_map { str_cookiepair.push_str(&format!("{}={}; ", cookie_k, cookie_v)); } // load requesttoken regex lazy_static! { static ref RE: Regex = Regex::new(r#"\{"token":"(?P[^"]*)"\}"#) .expect("Error while parsing the requesttoken regex"); } let post_body = login_get.body().await.map_err(|e| { eprintln!("error_login_get_body: {}", e); crash(get_lang(&req), "error_login_get_body") })?; let post_body_str = String::from_utf8_lossy(&post_body); // save requesttoken (CSRF) for POST let requesttoken = RE .captures(&post_body_str) .ok_or_else(|| { eprintln!("error_login_regex (no capture)"); crash(get_lang(&req), "error_login_regex") })? .name("token") .ok_or_else(|| { eprintln!("error_login_regex (no capture named token)"); crash(get_lang(&req), "error_login_regex") })? .as_str(); // 2. POST /login let mut login_post = client .post(format!("{}/{}", CONFIG.nextcloud_url, "login")) .timeout(Duration::new(PROXY_TIMEOUT, 0)) .header("User-Agent", USER_AGENT); // include all NC cookies in one cookie (cookie pair) login_post = login_post.header("Cookie", str_cookiepair); // send the same POST data as you'd log in from a web browser let response_post = login_post .send_form(&NCLoginForm { user, password, timezone: "UTC", timezone_offset: "2", requesttoken, }) .await .map_err(|e| { eprintln!("error_login_post: {}", e); crash(get_lang(&req), "error_login_post") })?; // 3. set the same cookies in the user's browser let mut user_response = HttpResponse::SeeOther(); for item in response_post.headers().clone().get_all("set-cookie") { user_response.header( "Set-Cookie", item.to_str().map_err(|e| { eprintln!("error_login_setcookie: {}", e); crash(get_lang(&req), "error_login_setcookie") })?, ); } // redirect to forms! Ok(user_response .header(http::header::LOCATION, "/apps/forms") .finish() .await .map_err(|e| { eprintln!("error_login_redir: {}", e); crash(get_lang(&req), "error_login_redir") })?) } // checks if the token seems valid before asking the db. // The token must be 45 bytes long and base64-encoded. // returns true if the token is valid pub fn check_token(token: &str) -> bool { let token_dec = base64::decode_config(token, URL_SAFE_NO_PAD); if let Ok(token_bytes) = token_dec { token_bytes.len() == 45 } else { false } } // generates a new token pub fn gen_token(size: usize) -> String { // Using /dev/random to generate random bytes let mut r = OsRng; let mut my_secure_bytes = vec![0u8; size]; r.fill_bytes(&mut my_secure_bytes); base64::encode_config(my_secure_bytes, URL_SAFE_NO_PAD) } // generates a random username composed of // an adjective, a name and a 4-byte base64-encoded token. // with the default list, that represents: // 141 * 880 = 124 080 // 255^4 / 2 = 2 114 125 312 (we lose approx. the half because of uppercase) // 2 114 125 312 * 124 080 = 2.623206687*10^14 possible combinations?? pub fn gen_name() -> String { // uppercasing gen_token because NC would probably refuse two // users with the same name but a different case // and that'd be a pain to debug format!( "{}{}-{}", list_rand(&ADJ_LIST), list_rand(&NAME_LIST), gen_token(4).to_uppercase() ) } pub fn list_rand(list: &[String]) -> &String { let mut rng = rand::thread_rng(); let roll = rng.gen_range(0..list.len() - 1); &list[roll] }