282 lines
10 KiB
Rust
282 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]
|
||
|
}
|