1
0
Fork 0
mirror of https://git.42l.fr/neil/sncf.git synced 2024-04-25 11:00:33 +02:00
sncf/src/account.rs

280 lines
8.8 KiB
Rust

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<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 /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<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", "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]
}