PluriTon/build/deb-rust-pluriton-interface/account.rs
2021-10-18 18:22:03 +02:00

281 lines
10 KiB
Rust

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]
}