use actix_web::client::{Client, ClientRequest}; use actix_web::{http, web, HttpRequest, HttpResponse}; use actix_session::Session; use askama::Template; use chrono::Utc; use csrf::{AesGcmCsrfProtection, CsrfProtection}; use std::time::Duration; use url::Url; use crate::account::*; use crate::config::get_csrf_key; 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 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); 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, } #[derive(Deserialize)] pub struct CsrfToken { pub csrf_token: String, } pub async fn forward_login( req: HttpRequest, s: Session, params: web::Path, client: web::Data, dbpool: web::Data, ) -> Result { // 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") })?; 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 .map_err(|e| { eprintln!("error_forwardlogin_db_get (diesel error): {}", e); crash(get_lang(&req), "error_forwardlogin_db_get") })? .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") })?); } } // 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); // do not check for existing admin tokens and force a new registration // check if the csrf token is OK 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("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 { debug("warn: missing CSRF token."); return Err(crash(lang, "error_csrf_cookie")); } 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); 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, ) }) .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") .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, s: Session) -> Result { let seed = AesGcmCsrfProtection::from_key(get_csrf_key()); 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") .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| { 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") })?) }