mirror of
https://git.42l.fr/neil/sncf.git
synced 2024-06-08 02:32:14 +02:00
280 lines
8.8 KiB
Rust
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]
|
|
}
|