424 lines
14 KiB
Rust
424 lines
14 KiB
Rust
|
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<Url>,
|
||
|
client: web::Data<Client>,
|
||
|
) -> Result<HttpResponse, TrainCrash> {
|
||
|
let route = req.uri().path();
|
||
|
|
||
|
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 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 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 link_lang: String,
|
||
|
}
|
||
|
|
||
|
pub async fn forward_login(
|
||
|
req: HttpRequest,
|
||
|
s: Session,
|
||
|
params: web::Path<LoginToken>,
|
||
|
client: web::Data<Client>,
|
||
|
dbpool: web::Data<DbPool>,
|
||
|
|
||
|
) -> Result<HttpResponse, TrainCrash> {
|
||
|
|
||
|
// 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")
|
||
|
})?);
|
||
|
}
|
||
|
}
|
||
|
//let route = req.uri().path();
|
||
|
//let lang_req = forge_from(
|
||
|
// &route,
|
||
|
// &req,
|
||
|
// &url,
|
||
|
// &client,
|
||
|
// )
|
||
|
// .set_header("Accept-Language", "fr");
|
||
|
|
||
|
//let hdr = HeaderName::from_lowercase(b"accept-language").unwrap();
|
||
|
//let val = HeaderValue::from_static("fr");
|
||
|
|
||
|
//let mutreq = &mut req;
|
||
|
|
||
|
//mutreq.headers().insert(hdr , val );
|
||
|
//
|
||
|
//The stuff above did not work - first because client req, second because
|
||
|
//immutable reference (it does not make sense to change the proper req,
|
||
|
//read and resend something new
|
||
|
//
|
||
|
// 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<CsrfToken>,
|
||
|
client: web::Data<Client>,
|
||
|
dbpool: web::Data<DbPool>,
|
||
|
) -> Result<HttpResponse, TrainCrash> {
|
||
|
|
||
|
|
||
|
let old_csrf_token = csrf_post.csrf_token.clone();
|
||
|
let lang = csrf_post.link_lang.clone();
|
||
|
// 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::<String>("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,
|
||
|
csrf_token: &old_csrf_token
|
||
|
}
|
||
|
.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<Url>,
|
||
|
client: &web::Data<Client>,
|
||
|
) -> 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<HttpResponse, TrainCrash> {
|
||
|
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::<String>("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")
|
||
|
})?)
|
||
|
}
|
||
|
|