use actix_web::client::Client; use actix_web::{http, web, HttpRequest, HttpResponse}; use base64::URL_SAFE_NO_PAD; use rand::rngs::OsRng; use rand::Rng; use rand::RngCore; use regex::Regex; use std::time::Duration; use crate::config::PROXY_TIMEOUT; use crate::config::{ADJ_LIST, NAME_LIST}; 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 } } pub fn has_admintoken(req: &HttpRequest) -> Option<&str> { let c = req.headers().get("Cookie")?.to_str().ok()?; if c.contains("sncf_admin_token") { 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 /login let mut login_get = client .get(format!("{}/{}", CONFIG.nextcloud_url, "login")) .timeout(Duration::new(PROXY_TIMEOUT, 0)) .header("User-Agent", "Actix-web") .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(); for h_value in login_get.headers().get_all("set-cookie") { str_cookiepair = format!( "{}; {}", str_cookiepair, h_value.clone().to_str().map_err(|e| { eprintln!("error_login_cookiepair: {}", e); crash(get_lang(&req), "error_login_cookiepair") })? ); } // load requesttoken regex lazy_static! { static ref RE: Regex = Regex::new(r#"requesttoken="(?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", "Actix-web"); // 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] }