use actix_web::client::{Client, ClientRequest}; use actix_web::{http, web, HttpRequest, HttpResponse}; use askama::Template; use chrono::Utc; use regex::Regex; use std::time::Duration; use url::Url; use crate::account::*; use crate::config::PAYLOAD_LIMIT; use crate::config::PROXY_TIMEOUT; use crate::database::methods::InsertableForm; use crate::database::structs::Form; use crate::debug; use crate::errors::{crash, TrainCrash}; use crate::sniff::*; use crate::templates::*; use crate::DbPool; use crate::CONFIG; pub async fn forward( req: HttpRequest, body: web::Bytes, url: web::Data, client: web::Data, ) -> Result { let route = req.uri().path(); // 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) { debug(&format!("Restricted route blocked: {}", route)); return Ok(web_redir("/").await.map_err(|e| { eprintln!("error_redirect: {}", e); crash(get_lang(&req), "error_redirect") })?); } let forwarded_req = forge_from(route, &req, &url, &client); // check the request before sending it // (prevents the user from sending some specific POST requests) if check_request(route, &body) { debug(&format!( "Restricted request: {}", String::from_utf8_lossy(&body) )); return Err(crash(get_lang(&req), "error_dirtyhacker")); } // send the request to the Nextcloud instance let mut res = forwarded_req.send_body(body).await.map_err(|e| { eprintln!("error_forward_resp: {}", e); crash(get_lang(&req), "error_forward_req") })?; let mut client_resp = HttpResponse::build(res.status()); // remove connection as per the spec // and content-encoding since we have to decompress the traffic to edit it // 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") { 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 if route == "/ocs/v2.php/apps/forms/api/v1/form" { // retreive the body from the request result let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { eprintln!("error_forward_resp: {}", e); crash(get_lang(&req), "error_forward_resp") })?; // if a new form is created, automatically set some fields. // this is very hackish but it works! for now. 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 )); let forged_body = format!( r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#, form_id ); let update_req = forge_from( "/ocs/v2.php/apps/forms/api/v1/form/update", &req, &url, &client, ) .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); crash(get_lang(&req), "error_forward_isanon") })?; debug(&format!("(new_form) Request returned {}", res.status())); } Ok(client_resp.body(response_body).await.map_err(|e| { eprintln!("error_forward_clientresp_newform: {}", e); crash(get_lang(&req), "error_forward_clientresp_newform") })?) } else { Ok( client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| { eprintln!("error_forward_clientresp_newform: {}", e); crash(get_lang(&req), "error_forward_clientresp_std") })?), ) } // check the response before returning it (unused) /*if check_response(route, &response_body) { return Ok(web_redir("/")); }*/ } #[derive(Deserialize)] pub struct LoginToken { pub token: String, } pub async fn forward_login( req: HttpRequest, 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) { debug("Incorrect admin token given in params."); debug(&format!("Token: {:#?}", params.token)); return Err(crash(get_lang(&req), "error_dirtyhacker")); } let conn = dbpool.get().map_err(|e| { eprintln!("error_forwardlogin_db: {}", e); crash(get_lang(&req), "error_forwardlogin_db") })?; // check if the link exists in DB. if it does, update lastvisit_at. let formdata = Form::get_from_token(¶ms.token, &conn) .map_err(|e| { 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") })?; // else, 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. pub async fn forward_register( req: HttpRequest, 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")); } } let nc_username = gen_name(); println!("gen_name: {}", nc_username); let nc_password = gen_token(45); // attempts to create the account create_account(&client, &nc_username, &nc_password, lang.clone()).await?; debug(&format!("Created user {}", nc_username)); let conn = dbpool.get().map_err(|e| { eprintln!("error_forwardregister_pool: {}", e); crash(lang.clone(), "error_forwardregister_pool") })?; let token = gen_token(45); // store the result in DB let form_result = Form::insert( InsertableForm { created_at: Utc::now().naive_utc(), lastvisit_at: Utc::now().naive_utc(), token: &token, nc_username: &nc_username, nc_password: &nc_password, }, &conn, ); if form_result.is_err() { return Err(crash(lang, "error_forwardregister_db")); } 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") })?, ) .await .map_err(|e| { eprintln!("error_tplrender_resp (TplLink): {}", e); crash(lang, "error_tplrender_resp") })?) } // create a new query destined to the nextcloud instance // needed to forward any query fn forge_from( route: &str, req: &HttpRequest, url: &web::Data, client: &web::Data, ) -> ClientRequest { let mut new_url = url.get_ref().clone(); new_url.set_path(route); new_url.set_query(req.uri().query()); // insert forwarded header if we can let mut forwarded_req = client .request_from(new_url.as_str(), req.head()) .timeout(Duration::new(PROXY_TIMEOUT, 0)); // attempt to remove basic-auth header forwarded_req.headers_mut().remove("authorization"); if let Some(addr) = req.head().peer_addr { forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) } else { forwarded_req } } fn web_redir(location: &str) -> HttpResponse { HttpResponse::SeeOther() .header(http::header::LOCATION, location) .finish() } pub async fn index(req: HttpRequest) -> Result { Ok(HttpResponse::Ok() .content_type("text/html") .body( TplIndex { lang: &get_lang(&req), } .render() .map_err(|e| { eprintln!("error_tplrender (TplIndex): {}", e); crash(get_lang(&req), "error_tplrender") })?, ) .await .map_err(|e| { eprintln!("error_tplrender_resp (TplIndex): {}", e); crash(get_lang(&req), "error_tplrender_resp") })?) }