|
|
- 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<Client>,
- user: &str,
- password: &str,
- lang: String,
- ) -> Result<String, TrainCrash> {
- 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("<statuscode>").ok_or_else(|| {
- eprintln!("error_createaccount_ncstatus_parse: start missing");
- crash(lang.clone(), "error_createaccount_ncstatus_parse")
- })? + 12;
- let status_end = response_body.find("</statuscode>").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::<u16>() {
- 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<Client>,
- req: &HttpRequest,
- user: &str,
- password: &str,
- ) -> Result<HttpResponse, TrainCrash> {
- 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)
- .header("Accept-Language" , "fr" )
- .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<id> 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<String, String> = 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::<Vec<&str>>();
- 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::<Vec<&str>>();
- 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<token>[^"]*)"\}"#)
- .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)
- .header("Accept-Language" , "fr" );
- // 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("Accept-Language", "fr" )
- .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]
- }
|