diff --git a/docker/build/deb-rust-sncf/forward.rs b/docker/build/deb-rust-sncf/forward.rs index 4baf9da..1a86c3e 100644 --- a/docker/build/deb-rust-sncf/forward.rs +++ b/docker/build/deb-rust-sncf/forward.rs @@ -1,14 +1,14 @@ use actix_web::client::{Client, ClientRequest}; use actix_web::{http, web, HttpRequest, HttpResponse}; +use actix_session::Session; use askama::Template; use chrono::Utc; -use regex::Regex; +use csrf::{AesGcmCsrfProtection, CsrfProtection}; use std::time::Duration; use url::Url; -use csrf::{AesGcmCsrfProtection, CsrfProtection}; -use crate::config::get_csrf_key; use crate::account::*; +use crate::config::get_csrf_key; use crate::config::PAYLOAD_LIMIT; use crate::config::PROXY_TIMEOUT; use crate::database::methods::InsertableForm; @@ -27,51 +27,41 @@ pub async fn forward( client: web::Data, ) -> Result { let route = req.uri().path(); - - - if route == "/link/email" { - let email_request_headers = &req; - let email_body = &body; - - - //let mut body = String::new(); - let forged_emailheaders = format!( - "{:?}", - email_request_headers - ); - let forged_emailbody = format!( - "{:?}", - email_body - ); - //let body = email_response_body.escape_ascii().to_string(); - - /*email_response_body.read_to_string(&mut body)?;*/ - // does not work, because body is in bytes format. - println!("da budy {}",forged_emailbody ); - println!("da headers {}",forged_emailheaders); - - use std::io::Write; - use std::fs::OpenOptions; - let mut f = OpenOptions::new() - .append(true) - .create(true) // Optionally create the file if it doesn't already exist - .open("/tmp/foo") - .expect("Unable to open file"); - - //let body_bytes = as_bytes!("{:?}", body); - - f.write_all(forged_emailheaders.as_bytes()).expect("Unable to write data"); - f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); +if route == "/link/email" { + //let email_body = &body; + //let mut body = String::new(); + //let forged_emailbody = format!( + // "{:?}", + // email_body + // ); + + //let body = email_response_body.escape_ascii().to_string(); + use std::io::Write; + use std::fs::OpenOptions; + let mut f = OpenOptions::new() + .append(true) + .create(true) // Optionally create the file if it doesn't already exist + .open("/var/tokmails/tuples.csv") + .expect("Unable to open file"); + //f.write_all(forged_emailheaders.as_bytes()).expect("Unable to write data"); + ////f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); + f.write_all(&body).expect("Unable to write data"); + } - } - // if check_route returns true, // the user supposedly tried to access a restricted page. // They get redirected to the main page. - if check_route(route) { + if route.starts_with("/apps/files") { + // exception for /apps/files: always redirect to /apps/forms + debug(&format!("Files route blocked: {}", route)); + return Ok(web_redir("/apps/forms").await.map_err(|e| { + eprintln!("error_redirect: {}", e); + crash(get_lang(&req), "error_redirect") + })?); + } else if check_route(route) { debug(&format!("Restricted route blocked: {}", route)); return Ok(web_redir("/").await.map_err(|e| { eprintln!("error_redirect: {}", e); @@ -85,8 +75,8 @@ pub async fn forward( // (prevents the user from sending some specific POST requests) if check_request(route, &body) { debug(&format!( - "Restricted request: {}", - String::from_utf8_lossy(&body) + "Restricted request: {}", + String::from_utf8_lossy(&body) )); return Err(crash(get_lang(&req), "error_dirtyhacker")); } @@ -103,11 +93,12 @@ pub async fn forward( // and basic-auth, because this feature is not needed. for (header_name, header_value) in res .headers() - .iter() - .filter(|(h, _)| *h != "connection" && *h != "content-encoding") + .iter() + .filter(|(h, _)| *h != "connection" && *h != "content-encoding") { client_resp.header(header_name.clone(), header_value.clone()); } + // sparing the use of a mutable body when not needed // For now, the body only needs to be modified when the route // is "create a new form" route @@ -123,8 +114,8 @@ pub async fn forward( let form_id = check_new_form(&response_body); if form_id > 0 { debug(&format!( - "New form. Forging request to set isAnonymous for id {}", - form_id + "New form. Forging request to set isAnonymous for id {}", + form_id )); let forged_body = format!( @@ -137,8 +128,8 @@ pub async fn forward( &url, &client, ) - .set_header("content-length", forged_body.len()) - .set_header("content-type", "application/json;charset=utf-8"); + .set_header("content-length", forged_body.len()) + .set_header("content-type", "application/json;charset=utf-8"); let res = update_req.send_body(forged_body).await.map_err(|e| { eprintln!("error_forward_isanon: {}", e); @@ -161,8 +152,8 @@ pub async fn forward( // check the response before returning it (unused) /*if check_response(route, &response_body) { - return Ok(web_redir("/")); - }*/ + return Ok(web_redir("/")); + }*/ } #[derive(Deserialize)] @@ -177,17 +168,11 @@ pub struct CsrfToken { pub async fn forward_login( req: HttpRequest, + s: Session, params: web::Path, client: web::Data, dbpool: web::Data, ) -> Result { - // if the user is already logged in, redirect to the Forms app - if is_logged_in(&req).is_some() { - return Ok(web_redir("/apps/forms").await.map_err(|e| { - eprintln!("error_redirect (1:/apps/forms/): {}", e); - crash(get_lang(&req), "error_redirect") - })?); - } // check if the provided token seems valid. If not, early return. if !check_token(¶ms.token) { @@ -201,6 +186,7 @@ pub async fn forward_login( crash(get_lang(&req), "error_forwardlogin_db") })?; + let moved_token = params.token.clone(); // check if the link exists in DB. if it does, update lastvisit_at. let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn)) .await @@ -208,109 +194,76 @@ pub async fn forward_login( eprintln!("error_forwardlogin_db_get (diesel error): {}", e); crash(get_lang(&req), "error_forwardlogin_db_get") })? - .ok_or_else(|| { - debug("Token not found."); - crash(get_lang(&req), "error_forwardlogin_notfound") - })?; + .ok_or_else(|| { + debug("error: Token not found."); + crash(get_lang(&req), "error_forwardlogin_notfound") + })?; + + // copy the token in cookies. + s.set("sncf_admin_token", &moved_token).map_err(|e| { + eprintln!("error_login_setcookie (in login): {}", e); + crash(get_lang(&req),"error_login_setcookie") + })?; + + // if the user is already logged in, skip the login process + // we don't care if someone edits their cookies, Nextcloud will properly + // check them anyway + if let Some(nc_username) = is_logged_in(&req) { + if nc_username.contains(&format!("nc_username={}", formdata.nc_username)) { + return Ok(web_redir("/apps/forms").await.map_err(|e| { + eprintln!("error_redirect (1:/apps/forms/): {}", e); + crash(get_lang(&req), "error_redirect") + })?); + } + } - // else, try to log the user in with DB data, then redirect. + // try to log the user in with DB data, then redirect. login(&client, &req, &formdata.nc_username, &formdata.nc_password).await } - - - // creates a NC account using a random name and password. // the account gets associated with a token in sqlite DB. +// POST /link route pub async fn forward_register( req: HttpRequest, + s: Session, csrf_post: web::Form, client: web::Data, dbpool: web::Data, ) -> Result { let lang = get_lang(&req); - // if the user is already logged in, redirect to the Forms app - if is_logged_in(&req).is_some() { - return Ok(web_redir("/apps/forms").await.map_err(|e| { - eprintln!("error_redirect (2:/apps/forms/): {}", e); - crash(get_lang(&req), "error_redirect") - })?); - } - - // if the user has already generated an admin token, redirect too - if let Some(token) = has_admintoken(&req) { - lazy_static! { - static ref RE: Regex = Regex::new(r#"sncf_admin_token=(?P[0-9A-Za-z_\-]*)"#) - .expect("Error while parsing the sncf_admin_token regex"); - } - let admin_token = RE - .captures(&token) - .ok_or_else(|| { - eprintln!("error_forwardregister_tokenparse (no capture)"); - crash(get_lang(&req), "error_forwardregister_tokenparse") - })? - .name("token") - .ok_or_else(|| { - eprintln!("error_forwardregister_tokenparse (no capture named token)"); - crash(get_lang(&req), "error_forwardregister_tokenparse") - })? - .as_str(); - // sanitize the token beforehand, cookies are unsafe - if check_token(&admin_token) { - return Ok( - web_redir(&format!("{}/admin/{}", CONFIG.sncf_url, &admin_token)) - .await - .map_err(|e| { - eprintln!("error_redirect (admin): {}", e); - crash(get_lang(&req), "error_redirect") - })?, - ); - } else { - debug("Incorrect admin token given in cookies."); - debug(&format!("Token: {:#?}", &admin_token)); - return Err(crash(lang, "error_dirtyhacker")); - } - } + // do not check for existing admin tokens and force a new registration // check if the csrf token is OK - if let Some(cookie_token) = has_csrftoken(&req) { - lazy_static! { - static ref RE: Regex = Regex::new(r#"sncf_csrf_cookie=(?P[0-9A-Za-z_\-]*)"#) - .expect("Error while parsing the sncf_csrf_cookie regex"); - } - let cookie_csrf_token = RE - .captures(&cookie_token) - .ok_or_else(|| { - eprintln!("error_csrf_cookie: no capture"); - crash(get_lang(&req), "error_csrf_cookie") - })? - .name("token") - .ok_or_else(|| { - eprintln!("error_csrf_cookie: no capture named token"); - crash(get_lang(&req), "error_csrf_cookie") - })? - .as_str(); - - let raw_ctoken = base64::decode_config(cookie_csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| { - eprintln!("error_csrf_cookie (base64): {}", e); - crash(get_lang(&req), "error_csrf_cookie") - })?; - - let raw_token = base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| { - eprintln!("error_csrf_token (base64): {}", e); - crash(get_lang(&req), "error_csrf_token") - })?; + let cookie_csrf_token = s.get::("sncf_csrf_token").map_err(|e| { + eprintln!("error_csrf_cookie: {}", e); + crash(get_lang(&req), "error_csrf_cookie") + })?; + if let Some(cookie_token) = cookie_csrf_token { + let raw_ctoken = + base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err( + |e| { + eprintln!("error_csrf_cookie (base64): {}", e); + crash(get_lang(&req), "error_csrf_cookie") + }, + )?; + + let raw_token = + base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD) + .map_err(|e| { + eprintln!("error_csrf_token (base64): {}", e); + crash(get_lang(&req), "error_csrf_token") + })?; let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); - let parsed_token = seed.parse_token(&raw_token).expect("token not parsed"); - let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("cookie not parsed"); + let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed"); + let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed"); if !seed.verify_token_pair(&parsed_token, &parsed_cookie) { debug("warn: CSRF token doesn't match."); return Err(crash(lang, "error_csrf_token")); } - } - else { + } else { debug("warn: missing CSRF token."); return Err(crash(lang, "error_csrf_cookie")); } @@ -333,39 +286,41 @@ pub async fn forward_register( let token_mv = token.clone(); // store the result in DB - let form_result = web::block(move || Form::insert( - InsertableForm { - created_at: Utc::now().naive_utc(), - lastvisit_at: Utc::now().naive_utc(), - token: token_mv, - nc_username, - nc_password, - }, - &conn, - )) + let form_result = web::block(move || { + Form::insert( + InsertableForm { + created_at: Utc::now().naive_utc(), + lastvisit_at: Utc::now().naive_utc(), + token: token_mv, + nc_username, + nc_password, + }, + &conn, + ) + }) .await; if form_result.is_err() { return Err(crash(lang, "error_forwardregister_db")); } + s.set("sncf_admin_token", &token).map_err(|e| { + eprintln!("error_login_setcookie (in register): {}", e); + crash(lang.clone(), "error_login_setcookie") + })?; Ok(HttpResponse::Ok() .content_type("text/html") - .set_header( - "Set-Cookie", - format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token), - ) .body( TplLink { lang: &lang, admin_token: &token, config: &CONFIG, - } - .render() - .map_err(|e| { - eprintln!("error_tplrender (TplLink): {}", e); - crash(lang.clone(), "error_tplrender") - })?, + } + .render() + .map_err(|e| { + eprintln!("error_tplrender (TplLink): {}", e); + crash(lang.clone(), "error_tplrender") + })?, ) .await .map_err(|e| { @@ -406,22 +361,28 @@ fn web_redir(location: &str) -> HttpResponse { .finish() } -pub async fn index(req: HttpRequest) -> Result { - +pub async fn index(req: HttpRequest, s: Session) -> Result { let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); - let (csrf_token, csrf_cookie) = seed.generate_token_pair(None, 43200) + let (csrf_token, csrf_cookie) = seed + .generate_token_pair(None, 43200) .expect("couldn't generate token/cookie pair"); + s.set("sncf_csrf_token", &base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)).map_err(|e| { + eprintln!("error_login_setcookie (in index): {}", e); + crash(get_lang(&req), "error_login_setcookie") + })?; + + let cookie_admin_token = s.get::("sncf_admin_token").map_err(|e| { + eprintln!("error_forwardregister_tokenparse (index): {}", e); + crash(get_lang(&req), "error_forwardregister_tokenparse") + })?; Ok(HttpResponse::Ok() .content_type("text/html") - .set_header( - "Set-Cookie", - format!("sncf_csrf_cookie={}; HttpOnly; SameSite=Strict", - base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD))) .body( TplIndex { lang: &get_lang(&req), csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD), + sncf_admin_token: cookie_admin_token, } .render() .map_err(|e| {