@ -0,0 +1,21 @@ | |||
FROM rust:slim | |||
WORKDIR /opt | |||
# Install needed dependecies | |||
RUN echo "deb http://deb.debian.org/debian/ stretch main contrib non-free" >> /etc/apt/sources.list | |||
RUN echo "deb-src http://deb.debian.org/debian/ stretch main contrib non-free" >> /etc/apt/sources.list | |||
RUN apt-get update && apt-cache search libssl | |||
RUN apt-get update && apt-get install -y \ | |||
build-essential checkinstall zlib1g-dev pkg-config libssl1.0-dev -y | |||
COPY pluriton-interface pluriton-interface | |||
WORKDIR /opt/pluriton-interface | |||
CMD cargo run --no-default-features | |||
@ -0,0 +1,48 @@ | |||
FROM rust:slim | |||
WORKDIR /opt | |||
# Install needed dependecies | |||
RUN echo "deb http://ftp.de.debian.org/debian unstable main contrib" | tee -a /etc/apt/sources.list | |||
RUN apt-get update && apt-get install -y libmysql++-dev git | |||
RUN git clone https://git.42l.fr/neil/sncf.git | |||
WORKDIR /opt/sncf | |||
COPY config.toml /opt/sncf/config.toml | |||
# graphics individualization | |||
COPY foorms_logo_beta.svg /opt/sncf/templates/assets/foorms_logo_beta.svg | |||
COPY white-background.png /opt/sncf/templates/assets/index-background.png | |||
COPY Digi_3corner.png /opt/sncf/templates/assets/flavicon.ico | |||
COPY index.css /opt/sncf/templates/assets/index.css | |||
COPY cloud.css /opt/sncf/templates/assets/cloud.css | |||
COPY bootstrap.min.css /opt/sncf/templates/assets/bootstrap.min.css | |||
COPY digitalcourage.css /opt/sncf/templates/assets/digitalcourage.css | |||
COPY index.html /opt/sncf/templates/index.html | |||
COPY link.html /opt/sncf/templates/link.html | |||
COPY forward.rs /opt/sncf/src/forward.rs | |||
#COPY templates.rs /opt/sncf/src/templates.rs | |||
# The written is just firstly a hack | |||
COPY lang.json /opt/sncf/lang.json | |||
CMD cargo run --no-default-features --features mysql | |||
@ -0,0 +1,281 @@ | |||
use actix_web::client::Client; | |||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||
use base64::URL_SAFE_NO_PAD; | |||
use percent_encoding::percent_decode_str; | |||
use rand::rngs::OsRng; | |||
use rand::Rng; | |||
use rand::RngCore; | |||
use regex::Regex; | |||
use std::collections::HashMap; | |||
use std::time::Duration; | |||
use crate::config::{ADJ_LIST, NAME_LIST, PROXY_TIMEOUT, USER_AGENT}; | |||
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 | |||
} | |||
} | |||
// 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 /csrftoken | |||
let mut login_get = client | |||
.get(format!("{}/{}", CONFIG.nextcloud_url, "csrftoken")) | |||
.timeout(Duration::new(PROXY_TIMEOUT, 0)) | |||
.header("User-Agent", USER_AGENT) | |||
.header("Accept-Language" , "fr" ) | |||
.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(); | |||
// remove duplicate oc<id> cookie (nextcloud bug) | |||
// leading to sncf being unable to forge logins | |||
let cookie_set = login_get.headers().get_all("set-cookie"); | |||
let mut cookie_map: HashMap<String, String> = HashMap::new(); | |||
for c in cookie_set { | |||
// get str version of cookie header | |||
let c_str = c.to_str().map_err(|e| { | |||
eprintln!("error_login_cookiepair (1): {}", e); | |||
crash(get_lang(&req), "error_login_cookiepair") | |||
})?; | |||
// percent decode | |||
let c_str = percent_decode_str(c_str).decode_utf8_lossy(); | |||
//then remove values after ';' | |||
let c_str_arr = c_str.split(';').collect::<Vec<&str>>(); | |||
let c_str = c_str_arr | |||
.first() | |||
.expect("error: cookiepair split does not have a first value. shouldn't happen."); | |||
// split cookie key and cookie value | |||
// split_once would work best but it's nightly-only for now | |||
let c_str_arr = c_str.split('=').collect::<Vec<&str>>(); | |||
let c_key = c_str_arr | |||
.first() | |||
.expect("error: cookie key split does not have a first value, shouldn't happen."); | |||
let c_value = c_str.replace(&format!("{}=", c_key), ""); | |||
if c_key != c_str { | |||
// if the key already exists in hashmap, replace its value | |||
// else, insert it | |||
if let Some(c_sel) = cookie_map.get_mut(*c_key) { | |||
*c_sel = c_value; | |||
} else { | |||
cookie_map.insert(c_key.to_string(), c_value); | |||
} | |||
} else { | |||
eprintln!("error_login_cookiepair (2)"); | |||
return Err(crash(get_lang(&req), "error_login_cookiepair")); | |||
} | |||
} | |||
for (cookie_k, cookie_v) in cookie_map { | |||
str_cookiepair.push_str(&format!("{}={}; ", cookie_k, cookie_v)); | |||
} | |||
// load requesttoken regex | |||
lazy_static! { | |||
static ref RE: Regex = Regex::new(r#"\{"token":"(?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", USER_AGENT) | |||
.header("Accept-Language" , "fr" ); | |||
// 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("Accept-Language", "fr" ) | |||
.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] | |||
} |
@ -0,0 +1,148 @@ | |||
.has-text-centered > * { | |||
text-align: center; | |||
} | |||
.c-subelem, .c-fullwidth > * { | |||
color: #2c2c2c; | |||
} | |||
.c-blue { | |||
} | |||
.c-blue > a { | |||
color: white; | |||
background: #4b97ca; | |||
width: 154px; | |||
height: 35px; | |||
} | |||
.c-flex { | |||
display: flex; | |||
flex-wrap: wrap; | |||
justify-content: space-evenly; | |||
} | |||
@media screen and (min-width:1280px) { | |||
.c-flex.c-flex-reverse { | |||
flex-direction: row-reverse; | |||
} | |||
.c-jumbo { | |||
padding: 1.5rem 0; | |||
} | |||
.c-subelem { | |||
padding: 0; | |||
max-width: 40vw; | |||
margin: auto 0; | |||
} | |||
} | |||
.c-jumbo.c-jumbo-big { | |||
min-height: 25rem; | |||
padding: 1rem; | |||
} | |||
.c-jumbo.c-jumbo-medium { | |||
min-height: 18rem; | |||
padding: 1rem; | |||
} | |||
.c-jumbo.c-jumbo-small { | |||
min-height: 10rem; | |||
padding: 1rem; | |||
} | |||
.c-button { | |||
display: block; | |||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
border-radius: 10pt; | |||
text-align: center; | |||
transition: all .2s ease-in-out; | |||
white-space: nowrap; | |||
cursor: pointer; | |||
text-decoration: none; | |||
padding: 0.4em; | |||
width: max-content; | |||
height: max-content; | |||
min-width: 154px; | |||
min-height: 35px; | |||
margin: 0.5rem; | |||
color: white; | |||
text-weight: bolder; | |||
} | |||
.c-button:only-child { | |||
margin: auto; | |||
} | |||
.c-button.c-big { | |||
font-size: x-large; | |||
} | |||
.c-subelem { | |||
margin: auto 2rem; | |||
padding: 1rem 0; | |||
width: 100%; | |||
} | |||
.c-img-shadow { | |||
height: auto; | |||
max-width: 100%; | |||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
border-radius: 2px; | |||
} | |||
.c-img-center { | |||
display: block; | |||
margin: auto; | |||
} | |||
.c-fullwidth { | |||
width: 100%; | |||
margin: auto 2rem; | |||
} | |||
@media screen and (max-width:1279px) { | |||
.c-no-margin-mobile { | |||
margin: 0 !important; | |||
} | |||
} | |||
.c-jumbo { | |||
padding: .5rem 0; | |||
width: 100%; | |||
} | |||
.c-fade-left { | |||
opacity: 0; | |||
transform: translateX(-100px); | |||
animation: fadeInLeft 2s ease-in-out both; | |||
} | |||
.c-fade-right { | |||
opacity: 0; | |||
transform: translateX(100px); | |||
animation: fadeInRight 2s ease-in-out both; | |||
} | |||
@keyframes fadeInLeft { | |||
0% { | |||
opacity: 0; | |||
transform: translateX(-100px); | |||
} | |||
100% { | |||
opacity: 1; | |||
transform: translateX(0); | |||
} | |||
} | |||
@keyframes fadeInRight { | |||
0% { | |||
opacity: 0; | |||
transform: translateX(100px); | |||
} | |||
100% { | |||
opacity: 1; | |||
transform: translateX(0); | |||
} | |||
} |
@ -0,0 +1,34 @@ | |||
# The address and port sncf will listen | |||
listening_address = "0.0.0.0" | |||
listening_port = 8000 | |||
# Public-facing domain for sncf. | |||
# includes protocol, FQDN and port, without the trailing slash. | |||
sncf_url = "http://basabuuka.org" | |||
# SQLite: path to the SQLite DB | |||
# PostgreSQL: postgres://user:password@address:port/database | |||
# MySQL: mysql://user:password@address:port/database | |||
database_path = "mysql://nextcloud:KF8zUh1q4HovFmBa6lnk7xCmvoonfBoE@nextcloud-db:3306/nextcloud" | |||
# IP address of the Nextcloud instance, including protocol and port | |||
nextcloud_url = "http://nextcloud-web:80" | |||
# Nextcloud admin account credentials | |||
# TODO hash adminpw | |||
admin_username = "sncf_admin" | |||
admin_password = "DieHeiligeKuhDerNacht1635" | |||
# How many days of inactivity for an admin token before deleting NC accounts | |||
prune_days = 40 | |||
# Displays route names and a lot of information | |||
debug_mode = true | |||
# Used to encrypt csrf tokens and csrf cookies. | |||
# Generate random bytes: openssl rand -base64 32 | |||
# Then paste the result in this variable | |||
cookie_key = "Af3v5KMNPmwYYBRRjm/W5ds1rHDdyCEvpxVTMLKEKl0=" | |||
# Don't touch this unless you know what you're doing | |||
config_version = 2 |
@ -0,0 +1,572 @@ | |||
/* This software is governed by the CeCILL-B license. If a copy of this license | |||
* is not distributed with this file, you can obtain one at | |||
* http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.txt | |||
* | |||
* Authors of STUdS (initial project) : Guilhem BORGHESI (borghesi@unistra.fr) and Raphaël DROZ | |||
* Authors of OpenSondage : Framasoft (https://github.com/framasoft) | |||
* | |||
* ============================= | |||
* | |||
* Ce logiciel est régi par la licence CeCILL-B. Si une copie de cette licence | |||
* ne se trouve pas avec ce fichier vous pouvez l'obtenir sur | |||
* http://www.cecill.info/licences/Licence_CeCILL_V2.1-fr.txt | |||
* | |||
* Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ | |||
* Auteurs d'OpenSondage : Framasoft (https://github.com/framasoft) | |||
*/ | |||
@font-face { | |||
font-family: "DejaVu Sans"; | |||
src: url('../fonts/DejaVuSans.ttf'); | |||
} | |||
body { | |||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; | |||
color:#333; | |||
background:#eee; | |||
} | |||
.trait { /* hr */ | |||
background-color: #EEE; | |||
height: 5px; | |||
margin: 5px 0; | |||
border: none; | |||
} | |||
.ombre { | |||
background-color: #FFF; | |||
box-shadow: -4px 6px 9px rgba(50, 50, 50, 0.5); | |||
margin: 15px auto 30px; | |||
} | |||
.hide { | |||
display: none; | |||
} | |||
/* Commentaires */ | |||
div.comment{ | |||
margin-bottom: 10px; | |||
border-left: 1px dashed #999; | |||
background: #F5F5F5; | |||
padding-top: 4px; | |||
padding-bottom: 4px; | |||
padding-left: 14px; | |||
} | |||
.comment_date { | |||
font-style: italic; | |||
font-size: 12px; | |||
letter-spacing: -0.7px; | |||
color: grey; | |||
} | |||
/* Règles générales */ | |||
a:focus { /* a11y */ | |||
outline:#000 dotted 1px; | |||
} | |||
header, footer { | |||
} | |||
main { | |||
margin-top: 20px; | |||
} | |||
header h1 { | |||
margin-top: 0; | |||
} | |||
.container { | |||
padding: 30px; | |||
} | |||
.container .jumbotron { | |||
padding: 20px 20px; | |||
border-radius: 2px; | |||
} | |||
.container .jumbotron p { | |||
font-size: 1em; | |||
} | |||
.container .jumbotron .btn-group >.btn { | |||
margin-bottom: 20px; | |||
white-space: normal; | |||
} | |||
.summary h4 { | |||
margin-top:0; | |||
} | |||
.summary { | |||
font-weight:bold; | |||
} | |||
.summary img { | |||
max-width:100px; | |||
} | |||
.alert { | |||
border-radius: 2px; | |||
} | |||
.very-small { | |||
font-size: 70%; | |||
} | |||
/* Effet sur les images en page d'accueil */ | |||
.opacity img { | |||
opacity: 0.8; | |||
} | |||
.opacity:hover img { | |||
opacity: 1; | |||
} | |||
.home-choice { | |||
margin-bottom:50px; | |||
} | |||
/* Description du sondage */ | |||
/* studs.php et adminstuds.php */ | |||
header .lead { | |||
padding: 10px 0; | |||
margin:0; | |||
} | |||
header form .input-group .form-control { | |||
margin-bottom: 20px; | |||
} | |||
header form .input-group .input-group-btn { | |||
vertical-align: top; | |||
} | |||
#admin-link, #public-link { | |||
cursor:text; | |||
} | |||
.admin-link, .public-link, | |||
.admin-link:hover, .public-link:hover { | |||
color:#333; | |||
text-decoration:none; | |||
border:none; | |||
} | |||
.jumbotron h3, .jumbotron .js-title { | |||
margin-bottom:20px; | |||
margin-top:0; | |||
} | |||
.poll-description { | |||
font-family: inherit; | |||
word-break: initial; | |||
} | |||
/** Description in markdown **/ | |||
.form-group .CodeMirror, .form-group .CodeMirror-scroll { | |||
min-height: 200px; | |||
} | |||
#description-form .CodeMirror { | |||
background-color: #f5f5f5; | |||
} | |||
.editor-toolbar { | |||
margin-top: 10px; | |||
background-color: #eee; | |||
} | |||
#poll_comments { | |||
margin-top: 10px; | |||
} | |||
h4.control-label { | |||
display: inline-block; | |||
max-width: 100%; | |||
margin-bottom: 5px; | |||
font-weight: 700; | |||
font-size: 14px; | |||
line-height: 1.42857; | |||
margin-top:0; | |||
} | |||
caption { | |||
padding: 0 10px 10px; | |||
font-weight:bold; | |||
} | |||
.results a.btn-default.btn-sm { | |||
padding: 3px 7px; | |||
font-size: 0.7em; | |||
} | |||
/* adminstuds.php */ | |||
#title-form h3 .btn-edit, | |||
#email-form .btn-edit, | |||
#description-form .btn-edit, | |||
#poll-rules-form .btn-edit, | |||
#poll-hidden-form .btn-edit, | |||
#expiration-form .btn-edit, | |||
#password-form .btn-edit, | |||
#name-form .btn-edit { | |||
position:absolute; | |||
left:-2000px; | |||
} | |||
#title-form .btn-edit:focus, | |||
#title-form h3:hover .btn-edit, | |||
#email-form .btn-edit:focus, | |||
#email-form:hover .btn-edit, | |||
#description-form .btn-edit:focus, | |||
#description-form:hover .btn-edit, | |||
#poll-rules-form .btn-edit:focus, | |||
#poll-rules-form:hover .btn-edit, | |||
#poll-hidden-form .btn-edit:focus, | |||
#poll-hidden-form:hover .btn-edit, | |||
#expiration-form .btn-edit:focus, | |||
#expiration-form:hover .btn-edit, | |||
#password-form .btn-edit:focus, | |||
#password-form:hover .btn-edit, | |||
#name-form .btn-edit:focus, | |||
#name-form:hover .btn-edit { | |||
position:relative !important; | |||
left:0; | |||
padding: 0 10px; | |||
} | |||
.js-desc textarea { | |||
margin-bottom:5px; | |||
} | |||
#author-form .form-control-static { | |||
margin-bottom:0; | |||
} | |||
#poll-rules-form p, #poll-hidden-form p, | |||
.jumbotron p.well { | |||
font-size:16px; | |||
} | |||
.jumbotron p { | |||
font-weight: normal; | |||
} | |||
/* Tableau du sondage */ | |||
#tableContainer { | |||
overflow-x:auto; | |||
margin:5px auto; | |||
} | |||
table.results { | |||
margin:0 auto; | |||
} | |||
table.results > tbody > tr:hover > td, | |||
table.results > tbody > tr:hover > th { | |||
opacity:0.85; | |||
} | |||
table.results > tbody > tr#vote-form:hover > td, | |||
table.results > tbody > tr#vote-form:hover > th { | |||
opacity:1; | |||
} | |||
table.results tbody td { | |||
text-align:center; | |||
padding:1px 5px; | |||
border-bottom: 2px solid white; | |||
border-top: 2px solid white; | |||
} | |||
table.results thead th { | |||
text-align:center; | |||
border:2px solid white; | |||
padding: 5px; | |||
min-width:40px; | |||
font-size:12px; | |||
max-width:100px; | |||
overflow:hidden; | |||
text-overflow:ellipsis; | |||
} | |||
table.results thead th img { | |||
max-width: 100%; | |||
} | |||
table.results thead .btn { | |||
margin: 0 auto; | |||
display: block; | |||
} | |||
table.results th.rbd.day, | |||
table.results th.rbd.bg-info, | |||
table.results td.rbd { | |||
border-right: 2px dotted white; | |||
} | |||
table.results th.bg-primary.month, | |||
table.results th.day, | |||
table.results th.bg-info { | |||
border-bottom:none; | |||
border-top:none; | |||
border-right: 2px dotted white; | |||
border-left: 2px dotted white; | |||
} | |||
table.results tbody th.bg-info { | |||
border-right: 2px solid white; | |||
border-left: 2px solid white; | |||
text-align:center; | |||
min-width:150px; | |||
} | |||
table.results th.bg-primary.month, | |||
table.results th.day { | |||
text-align:left; | |||
} | |||
table.results #nom { | |||
width:115px; | |||
} | |||
table.results .btn-link.btn-sm { | |||
padding:2px; | |||
} | |||
#addition { | |||
vertical-align:top; | |||
} | |||
#showChart { | |||
margin-top:30px; | |||
} | |||
#Chart { | |||
padding-right:30px; | |||
} | |||
/* Formulaire de création de sondage */ | |||
@media (max-width: 767px) { | |||
#formulaire .col-xs-12 { | |||
padding-left: 0; | |||
margin-bottom: 20px; | |||
} | |||
} | |||
/* Formulaire de vote */ | |||
#vote-form td ul, #vote-form td label { | |||
margin:0; | |||
font-size:12px; | |||
} | |||
#vote-form td label { | |||
padding: 1px 3px; | |||
} | |||
#vote-form td { | |||
border-top:2px solid white; | |||
} | |||
#vote-form td:first-child { | |||
min-width: 180px; | |||
} | |||
.yes input, .ifneedbe input,.no input { | |||
position: absolute; | |||
width: 1px; | |||
height: 1px; | |||
padding: 0; | |||
margin: -1px; | |||
overflow: hidden; | |||
clip: rect(0px, 0px, 0px, 0px); | |||
border: 0 none; | |||
} | |||
.choice input:focus + label { | |||
outline: 2px dotted #000; | |||
outline-offset: -2px; | |||
} | |||
.choice { | |||
width: 35px; | |||
margin:0 auto !important; | |||
} | |||
.choice label { | |||
cursor: pointer; | |||
} | |||
td.btn-edit { | |||
padding: 5px; | |||
} | |||
span.edit-username-left { | |||
float: right; | |||
} | |||
.yes .btn, .ifneedbe .btn, .no .btn { | |||
width: 35px; | |||
color: #555; | |||
} | |||
.yes .btn,.yes .btn:hover { | |||
border-bottom-right-radius:0 !important; | |||
border-bottom-left-radius:0 !important; | |||
margin-bottom:-1px !important; | |||
margin-top:4px !important; | |||
color: #677835; | |||
} | |||
.ifneedbe .btn,.ifneedbe .btn:hover { | |||
border-radius: 0; | |||
color: #C48A1B; | |||
} | |||
.no .btn,.no .btn:hover{ | |||
border-top-right-radius:0 !important; | |||
border-top-left-radius:0 !important; | |||
margin-bottom:4px !important; | |||
margin-top:-1px !important; | |||
color: #AD220F; | |||
} | |||
.yes input[type="radio"]:checked + label { /* =.btn-success.active */ | |||
color: #fff; | |||
background-color: #768745; | |||
border-color: #67753C; | |||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||
} | |||
.ifneedbe input[type="radio"]:checked + label { /* =.btn-warning.active */ | |||
color: #fff; | |||
background-color: #CF9800; | |||
border-color: #BD8A00; | |||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||
} | |||
.no input[type="radio"]:checked + label { /* =.btn-danger.active */ | |||
color: #fff; | |||
background-color: #BF2511; | |||
border-color: #AD220F; | |||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.125) inset; | |||
} | |||
/* Button and results "No" */ | |||
.no .btn.startunchecked { | |||
box-shadow:none !important; | |||
color:#AD220F !important; | |||
background:#fff !important; | |||
border-color:#bdbdbd !important; | |||
} | |||
.no .btn.startunchecked:hover { | |||
background-color: #E0E0E0 !important; | |||
border-color: #949494 !important; | |||
} | |||
table.results .bg-danger .glyphicon { | |||
opacity:0; | |||
-moz-animation-name: hideNoIcon; | |||
-moz-animation-iteration-count: 1; | |||
-moz-animation-timing-function: ease-in; | |||
-moz-animation-duration: 2s; | |||
-webkit-animation-name: hideNoIcon; | |||
-webkit-animation-iteration-count: 1; | |||
-webkit-animation-timing-function: ease-in; | |||
-webkit-animation-duration: 2s; | |||
animation-name: hideNoIcon; | |||
animation-iteration-count: 1; | |||
animation-timing-function: ease-in; | |||
animation-duration: 2s; | |||
} | |||
@-moz-keyframes hideNoIcon { | |||
0% { | |||
opacity:1; | |||
} | |||
100% { | |||
opacity:0; | |||
} | |||
} | |||
@-webkit-keyframes hideNoIcon { | |||
0% { | |||
opacity:1; | |||
} | |||
100% { | |||
opacity:0; | |||
} | |||
} | |||
@keyframes hideNoIcon { | |||
0% { | |||
opacity:1; | |||
} | |||
100% { | |||
opacity:0; | |||
} | |||
} | |||
table.results > tbody > tr:hover > td .glyphicon { | |||
opacity:1 | |||
} | |||
/* create_date_poll.php */ | |||
#selected-days .form-group { | |||
margin-left:0; | |||
margin-right:0; | |||
} | |||
#selected-days legend input { | |||
box-shadow: none; | |||
border-width:0; | |||
color: #333; | |||
font-size: 21px; | |||
border-radius:0; | |||
margin-bottom:-1px; | |||
background:transparent; | |||
} | |||
#selected-days legend input:hover, | |||
#selected-days legend input:focus { | |||
border-bottom-width:1px; | |||
background-color:#E6E6E6; | |||
} | |||
#selected-days legend .input-group-addon { | |||
border:none; | |||
background:transparent; | |||
} | |||
#selected-days legend .input-group-addon:last-of-type { | |||
padding-top: 0; | |||
padding-bottom: 0; | |||
} | |||
#selected-days legend { | |||
height: 33px; | |||
} | |||
/* create_classic_poll.php */ | |||
.md-a-img { | |||
text-decoration:none !important; | |||
} | |||
#md-a-imgModal .form-group { | |||
margin:10px 0; | |||
} | |||
#md-a-imgModalLabel { | |||
font-size: 24px; | |||
} | |||
/* Admin */ | |||
#poll_search { | |||
cursor: pointer; | |||
} | |||
.table-of-polls { | |||
overflow-x: scroll; | |||
margin-bottom: 0; | |||
border: 0; | |||
box-shadow: none; | |||
} | |||
/* Studs */ | |||
.password_request { | |||
padding-top: 15px; | |||
padding-bottom: 15px; | |||
} | |||
#password-form .btn-cancel { | |||
float: right; | |||
} | |||
/* Buttons */ | |||
.btn { | |||
white-space: normal; | |||
} |
@ -0,0 +1,29 @@ | |||
<!doctype html> | |||
<html lang="{{ lang }}"> | |||
<head> | |||
<title>{{ "error_title"|tr(lang) }}</title> | |||
<meta charset="utf-8" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.ico" /> | |||
<link rel="stylesheet" href="/assets/index.css?v=1.0" /> | |||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||
<body> | |||
<div class="flex page-heading error fullheight"> | |||
<div class="flex page-heading-text"> | |||
<div> | |||
<h1 class="title">{{ "error_title"|tr(lang) }}</h1> | |||
<h2 class="title">{{ "error_description"|tr(lang) }}</h2> | |||
<h3 class="title">{{ error_msg|tr(lang) }}</h3> | |||
<p class="title">{{ "error_note1"|tr(lang) }}</h3> | |||
<p class="title">{{ "error_note2"|tr(lang) }}</h3> | |||
</div> | |||
</div> | |||
<div class="flex"> | |||
<a class="ncstyle-button error c-button" href="/">{{ "error_back"|tr(lang) }}</a> | |||
</div> | |||
</div> | |||
</body> | |||
</html> | |||
@ -0,0 +1 @@ | |||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27.38 31.61"><defs><style>.cls-1{fill:#fc0;}</style></defs><polygon class="cls-1" points="0 0 27.38 15.8 0 31.61 0 0"/></svg> |
@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 178.31 41.22"><defs><style>.cls-1{font-size:37.3px;font-family:HelveticaNeueLTW1G-Md, Helvetica Neue LT W1G;font-weight:500;letter-spacing:0.04em;}.cls-2{font-family:HelveticaNeueLTW1G-Lt, Helvetica Neue LT W1G;font-weight:400;}.cls-3{fill:#f0c;}.cls-4{font-size:6.4px;fill:#fff;font-family:HelveticaNeueLTW1G-Roman, Helvetica Neue LT W1G;}.cls-5{letter-spacing:-0.08em;}.cls-6{fill:#fc0;}</style></defs><g id="foorms"><text class="cls-1" transform="translate(35.73 31.97)">f<tspan class="cls-2" x="13.24" y="0">oorms</tspan></text></g><g id="beta"><rect class="cls-3" x="159.95" y="8.92" width="18.35" height="7.99" rx="2.26"/><text class="cls-4" transform="translate(161.29 15.23)">BE<tspan class="cls-5" x="8.3" y="0">T</tspan><tspan x="11.48" y="0">A</tspan></text></g><g id="Dreieck"><polygon class="cls-6" points="0 3.82 27.38 19.62 0 35.43 0 3.82"/></g></svg> |
@ -0,0 +1,423 @@ | |||
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") | |||
})?) | |||
} | |||
@ -0,0 +1,390 @@ | |||
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" { | |||
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(&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 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<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") | |||
})?); | |||
} | |||
} | |||
// 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 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::<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, | |||
} | |||
.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") | |||
})?) | |||
} | |||
@ -0,0 +1,421 @@ | |||
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 csrf::{AesGcmCsrfProtection, CsrfProtection}; | |||
use crate::config::get_csrf_key; | |||
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<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/tuple") | |||
.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) { | |||
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, | |||
params: web::Path<LoginToken>, | |||
client: web::Data<Client>, | |||
dbpool: web::Data<DbPool>, | |||
) -> Result<HttpResponse, TrainCrash> { | |||
// 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 = 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("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, | |||
csrf_post: web::Form<CsrfToken>, | |||
client: web::Data<Client>, | |||
dbpool: web::Data<DbPool>, | |||
) -> Result<HttpResponse, TrainCrash> { | |||
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<token>[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")); | |||
} | |||
} | |||
// 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<token>[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 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"); | |||
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")); | |||
} | |||
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<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) -> 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"); | |||
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), | |||
} | |||
.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") | |||
})?) | |||
} | |||
@ -0,0 +1,376 @@ | |||
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 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<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") | |||
})?); | |||
} | |||
} | |||
// 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 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::<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, | |||
} | |||
.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") | |||
})?) | |||
} | |||
@ -0,0 +1,292 @@ | |||
@font-face { | |||
font-family: 'Ubuntu-R'; | |||
src: url('/assets/Ubuntu-R.ttf'); | |||
font-weight: normal; | |||
font-style: normal; | |||
} | |||
.hidden { | |||
display: none !important; | |||
} | |||
* { | |||
font-family: Ubuntu,"Ubuntu-R",sans-serif; | |||
} | |||
a { | |||
text-decoration: none; | |||
/*color: #2359fb;*/ | |||
} | |||
.flex { | |||
display: flex; | |||
flex-wrap: wrap; | |||
justify-content: center; | |||
} | |||
.fullheight { | |||
min-height: 100vh; | |||
} | |||
.fullheight-nav { | |||
min-height: calc(100vh - 50px); | |||
} | |||
.fullwidth { | |||
width: 100%; | |||
text-align: center; | |||
} | |||
.title { | |||
color: black; | |||
/*text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18);*/ | |||
} | |||
h1 { | |||
font-size: 4vw; | |||
} | |||
h2 { | |||
font-size: 2.25vw; | |||
} | |||
h3 { | |||
font-size: 17pt bold; | |||
text-align: left; | |||
} | |||
p { | |||
font-size: 15pt medium; | |||
/*line-height: 1.6;*/ | |||
text-align: left; | |||
} | |||
.beta-tag { | |||
background: #ff00ff; | |||
color: white; | |||
border-radius: 5px; | |||
font-size: 0.9rem; | |||
padding: 0.3rem; | |||
margin-left: 0.5rem; | |||
} | |||
.beta-banner a { | |||
color: #ff00ff; | |||
} | |||
.beta-banner { | |||
background: repeating-linear-gradient( 45deg, #ff00ff, #ff00ff 10px, #c44c05 10px, #c44c05 20px ); | |||
color: white; | |||
padding: 1rem; | |||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
} | |||
.logo { | |||
width: 10vw; | |||
margin-right: 2vw; | |||
} | |||
.page-heading { | |||
background-image: url("/assets/index-background.png"); /*, linear-gradient(0deg, #1f58c6 0%, #1c66f2 100%);*/ | |||
background-position: 50% 50%; | |||
background-repeat: no-repeat; | |||
background-size: cover; | |||
background-attachment: fixed; | |||
} | |||
.page-heading-text { | |||
width: auto; | |||
margin: auto; | |||
padding: 1rem; | |||
} | |||
.page-heading > p { | |||
color: black; | |||
} | |||
.page-heading > p > a { | |||
color: #000000; | |||
} | |||
.page-heading.error { | |||
background: url("/assets/index-background.png"); /*, linear-gradient(0deg, #790000 0%, #a40000 100%)*/ | |||
} | |||
.ncstyle-button.error { | |||
background: #ee4040; | |||
} | |||
.error.ncstyle-button:hover { | |||
background: #c82323; | |||
} | |||
.navbar { | |||
height: 50px; | |||
} | |||
body, html { | |||
margin: 0; | |||
padding: 0; | |||
} | |||
.ncstyle-button { | |||
background-color: #ffcc00; | |||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
border-radius: 1vw; | |||
text-decoration: none; | |||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
white-space: nowrap; | |||
height: 48px; | |||
width: auto; | |||
line-height: 2.25rem; | |||
padding: 0.5em; | |||
background: #ffcc00; | |||
font-size: 20pt; | |||
min-width: 18vw; | |||
display: block; | |||
transition: all .25s ease-in-out; | |||
color: white; | |||
} | |||
.margin-bottom { | |||
margin-bottom: 1rem; | |||
} | |||
.ncstyle-button_blue:hover { | |||
background: #fbc617; | |||
} | |||
.ncstyle-button_yellow:hover { | |||
background: #fbc617; | |||
} | |||
.ncstyle-input { | |||
margin: auto; | |||
padding: 7px 6px; | |||
font-size: 16px; | |||
background-color: white; | |||
color: #454545; | |||
border: 1px solid #dbdbdb; | |||
outline: none; | |||
border-radius: 3px; | |||
cursor: text; | |||
width: 80vw; | |||
} | |||
.click { | |||
cursor: pointer; | |||
} | |||
#script-copy { | |||
display: none; | |||
} | |||
@media only screen and (max-width: 1080px) { | |||
h1 { | |||
font-size: 48px; | |||
} | |||
h2 { | |||
font-size: 32px; | |||
} | |||
h3 { | |||
font-size: 24px; | |||
} | |||
p { | |||
font-size: 16px; | |||
} | |||
.title { | |||
text-align: center; | |||
} | |||
.logo { | |||
width: 20vw; | |||
margin: 0; | |||
} | |||
.ncstyle-button_blue { | |||
font-size: 24px; | |||
} | |||
} | |||
@media only screen and (max-width: 1080px), screen and (max-height: 600px) { | |||
.scroll-down-arrow { | |||
display: none; | |||
} | |||
} | |||
.scroll-down-arrow { | |||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2hldnJvbl90aGluX2Rvd24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDIwIDIwIiBmaWxsPSJ3aGl0ZSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTE3LjQxOCw2LjEwOWMwLjI3Mi0wLjI2OCwwLjcwOS0wLjI2OCwwLjk3OSwwYzAuMjcsMC4yNjgsMC4yNzEsMC43MDEsMCwwLjk2OWwtNy45MDgsNy44M2MtMC4yNywwLjI2OC0wLjcwNywwLjI2OC0wLjk3OSwwbC03LjkwOC03LjgzYy0wLjI3LTAuMjY4LTAuMjctMC43MDEsMC0wLjk2OWMwLjI3MS0wLjI2OCwwLjcwOS0wLjI2OCwwLjk3OSwwTDEwLDEzLjI1TDE3LjQxOCw2LjEwOXoiLz48L3N2Zz4=); | |||
background-size: contain; | |||
background-repeat: no-repeat; | |||
} | |||
.scroll-down-link { | |||
cursor:pointer; | |||
height: 60px; | |||
width: 80px; | |||
margin: 0px 0 0 -40px; | |||
line-height: 60px; | |||
position: absolute; | |||
left: 50%; | |||
bottom: 10px; | |||
color: #FFF; | |||
text-align: center; | |||
font-size: 70px; | |||
z-index: 100; | |||
text-decoration: none; | |||
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.4); | |||
animation: fade_move_down 2s ease-in-out infinite; | |||
} | |||
/*animated scroll arrow animation*/ | |||
@keyframes fade_move_down { | |||
0% { transform:translate(0,-20px); opacity: 0; } | |||
50% { opacity: 1; } | |||
100% { transform:translate(0,20px); opacity: 0; } | |||
} | |||
.lds-ring { | |||
display: inline-block; | |||
position: relative; | |||
width: 80px; | |||
height: 80px; | |||
} | |||
.lds-ring div { | |||
box-sizing: border-box; | |||
display: block; | |||
position: absolute; | |||
width: 64px; | |||
height: 64px; | |||
margin: 8px; | |||
border: 8px solid #fff; | |||
border-radius: 50%; | |||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; | |||
border-color: #fff transparent transparent transparent; | |||
} | |||
.lds-ring div:nth-child(1) { | |||
animation-delay: -0.45s; | |||
} | |||
.lds-ring div:nth-child(2) { | |||
animation-delay: -0.3s; | |||
} | |||
.lds-ring div:nth-child(3) { | |||
animation-delay: -0.15s; | |||
} | |||
@keyframes lds-ring { | |||
0% { | |||
transform: rotate(0deg); | |||
} | |||
100% { | |||
transform: rotate(360deg); | |||
} | |||
} | |||
@ -0,0 +1,349 @@ | |||
<div id="container"> | |||
<!doctype html> | |||
<html lang="{{ "lang_code"|tr(lang) }}"> | |||
<head> | |||
<title>{{ "index_title"|tr(lang) }} – {{ "index_description"|tr(lang) }}</title> | |||
<meta charset="utf-8" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.svg" /> | |||
<link rel="stylesheet" href="/assets/index.css?v=1.2" /> | |||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||
<link rel="stylesheet" href="/assets/digitalcourage.css" /> | |||
<link rel="stylesheet" href="/assets/bootstrap.min.css" /> | |||
<style> | |||
.break { | |||
flex-basis: 100%; | |||
height: 0; | |||
} | |||
.grid-container { | |||
display: grid; | |||
grid-template-columns: auto auto; | |||
width: 725px; | |||
grid-gap: 25px; | |||
} | |||
.grid-container2 { | |||
display: grid; | |||
grid-template-columns: auto auto auto; | |||
width: 532px; | |||
grid-gap: 35px; | |||
} | |||
@media only screen and (max-width: 768px) { | |||
/* For mobile phones: */ | |||
[class*="grid-container"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
justify-content: center; | |||
} | |||
[class*="grid-container2"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
justify-content: center; | |||
grid-gap: 20px; | |||
} | |||
[class*="item2"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
align-items: center; | |||
} | |||
} | |||
.div_120 { | |||
flex-basis: 100%; | |||
height: 120px; | |||
} | |||
.div_60 { | |||
flex-basis: 100%; | |||
height: 60px; | |||
} | |||
.div_45 { | |||
flex-basis: 100%; | |||
height: 45px; | |||
} | |||
.div_35 { | |||
flex-basis: 100%; | |||
height: 35px; | |||
} | |||
.div_25 { | |||
flex-basis: 100%; | |||
height: 25px; | |||
} | |||
.div_10 { | |||
flex-basis: 100%; | |||
height: 10px; | |||
} | |||
.item1 { | |||
width: 350px; | |||
height: 200px; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.item2 { | |||
width: 350px; | |||
height: 200px; | |||
#display: flex; | |||
#justify-content: center; | |||
#align-items: center; | |||
} | |||
.h3 { | |||
font-size: 20pt; | |||
} | |||
h2 { | |||
font-size: 30pt; | |||
} | |||
.a1 { | |||
font-size: 20pt; | |||
} | |||
p { | |||
font-size: 14pt; | |||
} | |||
p1 { | |||
font-size: 20pt; | |||
} | |||
.downDC { | |||
height: 90px; | |||
padding: 10px; | |||
} | |||
.c-img-shadow { | |||
height: 200px; | |||
max-width: 100%; | |||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
border-radius: 2px; | |||
} | |||
</style> | |||
<noscript><style> .jsonly { display: none } </style></noscript> | |||
<script> | |||
window.onload = function() { | |||
// retrieved from server-side template | |||
let csrf_token = "{{ csrf_token }}"; | |||
let lang = "{{ lang }}"; | |||
document.getElementById('langs').value=lang; | |||
document.getElementById('new_link_button1').addEventListener('click', function () { | |||
new_link(csrf_token); | |||
}); | |||
document.getElementById('new_link_button2').addEventListener('click', function () { | |||
new_link(csrf_token); | |||
}); | |||
} | |||
function getSelectedOption(sel) { | |||
var opt; | |||
for ( var i = 0, len = sel.options.length; i < len; i++ ) { | |||
opt = sel.options[i]; | |||
if ( opt.selected === true ) { | |||
break; | |||
} | |||
} | |||
return opt; | |||
} | |||
function new_link(csrf) { | |||
var sel = document.getElementById('langs'); | |||
let opt = getSelectedOption(sel); | |||
let lang = opt.value; | |||
document.getElementById('langs').value = lang; | |||
document.getElementById("link_lang").value = lang; | |||
document.getElementById("csrf_token").value = csrf; | |||
document.getElementById('new_link').submit(); | |||
document.getElementById('new_link_button').classList.add("hidden"); | |||
document.getElementById('loading_ring').classList.remove("hidden"); | |||
} | |||
</script> | |||
</head> | |||
<body> | |||
<div class="container ombre"> | |||
<header role="banner" class="clearfix"> | |||
<form method="get" action="/" class="hidden-print"> | |||
<div class="input-group input-group-sm pull-right col-xs-12 col-sm-2 langs" style="margin-right: 8px"> | |||
<select id="langs" name="lang" class="form-control" title="Select language" > | |||
<option lang="fr" value="fr">Français</option> | |||
<option lang="en" selected value="en">English</option> | |||
<option lang="oc" value="oc">Occitan</option> | |||
<option lang="es" value="es">Español</option> | |||
<option lang="de" value="de">Deutsch</option> | |||
<option lang="nl" value="nl">Dutch</option> | |||
<option lang="it" value="it">Italiano</option> | |||
<option lang="br" value="br">Brezhoneg</option> | |||
</select> | |||
<span class="input-group-btn"> | |||
<button type="submit" id="language_button" class="btn btn-default btn-sm language_button" title="Change language">OK</button> | |||
</span> | |||
</div> | |||
</form> | |||
<a href="https://foorms.digitalcourage.de/" title="Home - foorms" style="margin-left: 8px" > | |||
<img src="/assets/foorms_logo_beta.svg" alt="foorms" class="" height="58vh" /> | |||
</a> | |||
<h2 class="lead col-xs-12"></h2> <div class="trait col-xs-12" role="presentation"></div> | |||
</header> | |||
<main role="main"> | |||
<div class="div_10"></div> | |||
<div class="div_10"></div> | |||
<div class="div_10"></div> | |||
<div class="flex has-text-centered"> | |||
<p> | |||
<div> | |||
<h2 class="title">{{ "index_title2"|tr(lang) }}</h2> | |||
</div> | |||
<br/> | |||
<div class="break"></div> | |||
</div> | |||
<div class="div_25"></div> | |||
<div class="flex has-text-centered"> | |||
<div> | |||
<h3 class="title">{{ "index_description"|tr(lang) }}</h3> | |||
</div> | |||
<div class="break"></div> | |||
<div> | |||
<h3 class="title">{{ "index_description2"|tr(lang) }}</h3> | |||
</div> | |||
</div> | |||
</p> | |||
<div class="div_60"></div> | |||
<div class="flex has-text-centered"> | |||
<div class=" flex"> | |||
<noscript> | |||
<a class="ncstyle-button">{{ "index_nojs"|tr(lang) }}</a> | |||
</noscript> | |||
<form id="new_link" action="/link" method="post"> | |||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||
<a id="new_link_button1" class="c-button ncstyle-button" >{{ "index_createform_button"|tr(lang) }}</a> </form> | |||
<div id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div> | |||
</div> | |||
</div> | |||
<div class="break"></div> | |||
<div class="div_120"></div> | |||
<div class="has-text-centered"> | |||
<h2>{{ "index_panel1_title"|tr(lang) }}</h2> | |||
</div> | |||
<div class="div_25"></div> | |||
<center> | |||
<div class="grid-container"> | |||
<div class="item1"> | |||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png" width="350px" height="200px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png" height="200px" width="350px" /></a> | |||
</div> | |||
<div class= "item2"> | |||
<h3 id="item2_header">{{ "index_panel2_title"|tr(lang) }}</h3> | |||
<p class="item2_paragraph">{{ "index_panel2_desc1"|tr(lang) }}</p><p class="item2_paragraph">{{ "index_panel2_desc2"|tr(lang) }}<a href="https://github.com/nextcloud/forms/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature%3A+%E2%9D%93+question+types%22">{{ "index_panel2_desc2_link"|tr(lang) }}</a>.</p> | |||
</div> | |||
</div> | |||
</br> | |||
<div class="grid2gridspace"></div> | |||
<div class="grid-container"> | |||
<div class="item1"> | |||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png" height="200px" width="350px" /></a> | |||
</div> | |||
<div class="item2"> | |||
<h3>{{ "index_panel3_title"|tr(lang) }}</h3> | |||
<p>{{ "index_panel3_desc1"|tr(lang) }}</p> | |||
</div> | |||
</div> | |||
</br> | |||
<div class="grid-container"> | |||
<div class="item1"> | |||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png" height="200px" width="350px" /></a> | |||
</div> | |||
<div class="item2"> | |||
<h3>{{ "index_panel4_title"|tr(lang) }}</h3> | |||
<p>{{ "index_panel4_desc1"|tr(lang) }}</p> | |||
</div> | |||
</div> | |||
</br> | |||
<div class="grid-container"> | |||
<div class="item1"> | |||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png" height="200px" width="350px"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png"height="200px" width="350px" /></a> | |||
</div> | |||
<div class="item2"> | |||
<h3>{{ "index_panel5_title"|tr(lang) }}</h3> | |||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p> | |||
<p>{{ "index_panel5_desc2"|tr(lang) }}</p> | |||
</div> | |||
</div> | |||
</br> | |||
<div class="grid-container"> | |||
<div class="item1"> | |||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png" height="200px" width="350px" /></a> | |||
</div> | |||
<div class="item2"> | |||
<h3>{{ "index_panel6_title"|tr(lang) }}</h3> | |||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p> | |||
</div> | |||
</div> | |||
<div class="div_60"></div> | |||
<div class="flex has-text-centered"> | |||
<div class=" flex"> | |||
<noscript> | |||
<a class="ncstyle-button">{{ "index_nojs"|tr(lang) }}</a> | |||
</noscript> | |||
<form id="new_link" action="/link" method="post"> | |||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||
<a id="new_link_button2" class="c-button ncstyle-button" >{{ "index_createform_button"|tr(lang) }}</a> </form> | |||
<div id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div> | |||
</div> | |||
</div> | |||
<div class="div_120"></div> | |||
<p> | |||
<div class="flex has-text-centered"> | |||
<div> | |||
<h2 class="title">{{ "index_disclaimer_title"|tr(lang) }}</h2> | |||
</div> | |||
<div class="break"></div> | |||
<div class="div_25"></div> | |||
<div> | |||
<p1 class="title">{{ "index_disclaimer1"|tr(lang) }}</p1> | |||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer2_link_org"|tr(lang) }}</a1> | |||
<p1 class="title">{{ "index_disclaimer2"|tr(lang) }}</p1> | |||
</div> | |||
<br> | |||
<div class="break"></div> | |||
<div> | |||
<p1 class="title">{{ "index_disclaimer2_but"|tr(lang) }}</p1> | |||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer2_link_don"|tr(lang) }}</a> | |||
</div> | |||
<br> | |||
<div class="break"></div> | |||
<div> | |||
<p1 class="title">{{ "index_disclaimer3"|tr(lang) }}</p1> | |||
<a href="https://www.digitalcourage.de" class="a1">{{ "index_disclaimer3_link"|tr(lang) }}</a> | |||
<p1 class="title">{{ "index_disclaimer4"|tr(lang) }}</p1> | |||
</div> | |||
</div> | |||
</p> | |||
<div class="div_120"></div> | |||
<div class="c-blue grid-container2"> | |||
<a href="https://42l.fr/Rapport-technique" style="font-size:15px" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a> | |||
<a href="https://git.42l.fr/neil/sncf" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a> | |||
<a href="https://git.42l.fr/neil/sncf/src/branch/root/LICENSE" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a> | |||
</div> | |||
<div class="div_10"></div> | |||
</center> | |||
</main> | |||
</div> <!-- .container --> | |||
<div class="container ombre downDC" style="display:flex;align-items:center;"> | |||
<h2 class="lead"><a target="_blank" href="https://digitalcourage.de/">Digitalcourage</a> | <a target="_blank" href="https://digitalcourage.de/newsletter">Newsletter</a> | <a target="_blank" href="https://digitalcourage.de/spenden">{{ "impressum_donations"|tr(lang) }}</a> | <a target="_blank" href="https://digitalcourage.de/en">Impressum</a> | <a target="_blank" href="https://digitalcourage.de/en">{{ "impressum_privacy"|tr(lang) }}</a> </h2> | |||
</div> | |||
</body> | |||
</html> |
@ -0,0 +1,520 @@ | |||
{ | |||
"lang_code": { | |||
"en": "en", | |||
"fr": "fr", | |||
"de": "de" | |||
}, | |||
"lang_full": { | |||
"en": "English", | |||
"fr": "Français", | |||
"de": "Deutsch" | |||
}, | |||
"meta_description": { | |||
"en": "foorms : create forms for free, without registration while protecting your privacy", | |||
"fr": "foorms : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée", | |||
"de": "foorms: erstellen Sie gratis Umfragen, ohne Registrierung und unter Wahrung Ihrer Privatssphäre" | |||
}, | |||
"impressum_donations": { | |||
"en": "Donations", | |||
"fr": "Dons", | |||
"de": "Spenden" | |||
}, | |||
"impressum_privacy": { | |||
"en": "Privacy", | |||
"fr": "Protection des données", | |||
"de": "Datenschutz" | |||
}, | |||
"index_title": { | |||
"en": "foorms", | |||
"fr": "foorms", | |||
"de": "foorms" | |||
}, | |||
"index_title2": { | |||
"en": "What is foorms?", | |||
"fr": "Qu'est-ce que c'est foorms?", | |||
"de": "Was ist foorms?" | |||
}, | |||
"index_title3": { | |||
"en": "How does foorms work?", | |||
"fr": "Comme foorms functionne?", | |||
"de": "Wie funktioniert foorms?" | |||
}, | |||
"index_description": { | |||
"en": "Create forms fast and simple - without registration,", | |||
"fr": "Créez des questionnaires en facon simple et vite - sans inscription,", | |||
"de": "Erstellen Sie schnell und einfach Umfragen - ohne Registrierung," | |||
}, | |||
"index_description2": { | |||
"en": "advertisement, tracking and saving of metadata.", | |||
"fr": "publicité, tracking et sauvegarde des métadonnées.", | |||
"de": "Werbung, Tracking und Speicherung von Metadaten." | |||
}, | |||
"index_beta_tag": { | |||
"en": "BETA", | |||
"fr": "BETA", | |||
"de": "BETA" | |||
}, | |||
"index_nojs": { | |||
"en": "Please enable JavaScript in your browser!", | |||
"fr": "Veuillez activer JavaScript dans votre navigateur !", | |||
"de": "Bitte aktivieren Sie JavaScript in ihrem Browser!" | |||
}, | |||
"index_createform_button": { | |||
"en": "Create a form", | |||
"fr": "Créer un formulaire", | |||
"de": "Umfrage erstellen" | |||
}, | |||
"index_continueform_button": { | |||
"en": "Access your forms", | |||
"fr": "Accéder à vos formulaires", | |||
"de": "Zu deinen Umfragen" | |||
}, | |||
"index_beta_banner_title": { | |||
"en": "Warning: Service in beta.", | |||
"fr": "Attention : Service en bêta.", | |||
"de": "Achtung: Seite in Beta Version" | |||
}, | |||
"index_beta_banner_desc1": { | |||
"en": "This service is currently under development and might behave in an unexpected way.", | |||
"fr": "Ce service est en cours de développement et pourrait se comporter de manière inattendue.", | |||
"de": "Diese Seite ist in Entwicklung und könnte sich unerwartet verhalten." | |||
}, | |||
"index_beta_banner_desc2": { | |||
"en": "Feel free to send feedbacks on our ", | |||
"fr": "Vous pouvez nous envoyer vos retours sur ", | |||
"de": "Feedback gerne an " | |||
}, | |||
"index_beta_banner_desc_link": { | |||
"en": "our contact page", | |||
"fr": "notre page de contact", | |||
"de": "unsere Kontaktseite" | |||
}, | |||
"index_disclaimer_title": { | |||
"en": "Who keeps foorms running?", | |||
"fr": "Qui a organisé foorms?", | |||
"de": "Wer betreibt foorms?" | |||
}, | |||
"index_disclaimer1": { | |||
"en": "This service is maintained for you from ", | |||
"fr": "Ce service vous est fourni gratuitement de ", | |||
"de": "Diese Seite wird von " | |||
}, | |||
"index_disclaimer2": { | |||
"en": " for free.", | |||
"fr": " gratuitement.", | |||
"de": " für Sie kostenlos angeboten" | |||
}, | |||
"index_disclaimer2_link_org": { | |||
"en": " Digitalcourage e.V. ", | |||
"fr": " Digitalcourage e.V. ", | |||
"de": " Digitalcourage e.V. " | |||
}, | |||
"index_disclaimer2_but": { | |||
"en": " But you have the possibility to ", | |||
"fr": " Mais vous avez la possibilité de ", | |||
"de": " Aber Sie können gern " | |||
}, | |||
"index_disclaimer2_link_don": { | |||
"en": "donate.", | |||
"fr": "faire une donation.", | |||
"de": "spenden." | |||
}, | |||
"index_disclaimer3": { | |||
"en": "Subscribe to the ", | |||
"fr": "Inscrivez-vous à notre ", | |||
"de": "Abonnieren Sie den " | |||
}, | |||
"index_disclaimer3_link": { | |||
"en": "newsletter, ", | |||
"fr": "newsletter, ", | |||
"de": "Newsletter, " | |||
}, | |||
"index_disclaimer4": { | |||
"en": " to stay informed about our work!", | |||
"fr": " pour rester informé de notre travail!", | |||
"de": " um über unsere Arbeit informiert zu bleiben!" | |||
}, | |||
"index_panel1_title": { | |||
"en": "How does foorms work?", | |||
"fr": "Comme foorms functionne?", | |||
"de": "Wie funktioniert foorms?" | |||
}, | |||
"index_panel1_desc1": { | |||
"en": "Are you searching for a privacy-friendly alternative to Google Forms while keeping its ease of use?", | |||
"fr": "Cherchez-vous une alternative éthique à Google Forms qui reste simple d'utilisation ?", | |||
"de": "Suchen Sie eine ethisch sinnvolle Alternative zu Google Forms, welche gleichzeitig einfach in der Bedienung ist?" | |||
}, | |||
"index_panel1_desc2": { | |||
"en": "You've just found it.", | |||
"fr": "Vous venez de la trouver.", | |||
"de": "Sie haben sie gefunden." | |||
}, | |||
"index_panel2_title": { | |||
"en": "Choose and order your fields", | |||
"fr": "Choisissez et ordonnez vos champs", | |||
"de": "Wählen und Ordnen Sie ihre Felder" | |||
}, | |||
"index_panel2_desc1": { | |||
"en": "The software currently supports seven field types.", | |||
"fr": "Pour le moment, le logiciel supporte sept types de champs.", | |||
"de": "Im Moment unterstützt die Software sieben Typen von Feldern." | |||
}, | |||
"index_panel2_desc2": { | |||
"en": "New field types are ", | |||
"fr": "De nouveaux types de champs sont ", | |||
"de": "Neue Typen von Feldern sind " | |||
}, | |||
"index_panel2_desc2_link": { | |||
"en": "currently in the works", | |||
"fr": "en cours d'élaboration", | |||
"de": "momentan in Bearbeitung" | |||
}, | |||
"index_panel3_title": { | |||
"en": "Analyze the answers", | |||
"fr": "Analysez les réponses", | |||
"de": "Analysieren Sie die Antworten" | |||
}, | |||
"index_panel3_desc1": { | |||
"en": "See detailed graphs of the answers to your form.", | |||
"fr": "Visualisez les réponses à votre formulaire avec un graphique.", | |||
"de": "Visualisieren Sie die Antworten Ihrer Umfrage graphisch." | |||
}, | |||
"index_panel4_title": { | |||
"en": "Export the answers", | |||
"fr": "Exportez les réponses", | |||
"de": "Export der Antworten" | |||
}, | |||
"index_panel4_desc1": { | |||
"en": "Export the raw data of your form in CSV format to integrate the answers in other software (e.g. LibreOffice Calc or Microsoft Excel).", | |||
"fr": "Exportez les données brutes de votre formulaire en format CSV pour intégrer les réponses dans d'autres logiciels (ex. LibreOffice Calc ou Microsoft Excel).", | |||
"de": "Exportieren Sie die Rohdaten Ihrer Umfrage im CSV Format um die Antworten in anderer Software zu integrieren( z.B. LibreOffice Calc)" | |||
}, | |||
"index_panel5_title": { | |||
"en": "Edit your form's settings", | |||
"fr": "Paramétrez vos formulaires", | |||
"de": "Einstellungen Ihrer Umfragen" | |||
}, | |||
"index_panel5_desc1": { | |||
"en": "Use the share link to send your form to other people.", | |||
"fr": "Utilisez le lien de partage pour envoyer votre formulaire à d'autres personnes.", | |||
"de": "Nutzen Sie den Teilen Link um Ihre Umfrage anderen Menschen zu schicken." | |||
}, | |||
"index_panel5_desc2": { | |||
"en": "You can also define an expiration date for your form.", | |||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire.", | |||
"de": "Sie können auch ein Ablaufdatum für ihre Umfrage festsetzen." | |||
}, | |||
"index_panel6_title": { | |||
"en": "All your forms in one place", | |||
"fr": "Tous vos formulaires au même endroit", | |||
"de": "Alle Ihre Umfragen an einem Ort" | |||
}, | |||
"index_panel6_desc1": { | |||
"en": "Find all your forms in the same panel.", | |||
"fr": "Retrouvez tous vos formulaires sur un même panel.", | |||
"de": "Finde alle deine Umfragen in einem Panel." | |||
}, | |||
"index_bottom_docs": { | |||
"en": "Documentation", | |||
"fr": "Documentation", | |||
"de": "Dokumentation" | |||
}, | |||
"index_bottom_source": { | |||
"en": "Source code", | |||
"fr": "Code source", | |||
"de": "Quellcode" | |||
}, | |||
"index_bottom_lic": { | |||
"en": "License", | |||
"fr": "Licence", | |||
"de": "Lizenz" | |||
}, | |||
"index_credits_title": { | |||
"en": "Credits", | |||
"fr": "Crédits", | |||
"de": "Credits" | |||
}, | |||
"index_credits_desc1": { | |||
"en": "The Nextcloud software suite and the Nextcloud Forms application has been developed by ", | |||
"fr": "La suite logicielle Nextcloud et l'application Nextcloud Forms a été développée par ", | |||
"de": "Die Nextcloud Software Sammlung und die Nextcloud Forms Applikation wurden entwickelt von " | |||
}, | |||
"index_credits_desc1_link": { | |||
"en": "the Nextcloud team", | |||
"fr": "l'équipe Nextcloud", | |||
"de": "dem Nextcloud Team" | |||
}, | |||
"index_credits_desc1_a": { | |||
"en": " and its contributors.", | |||
"fr": " et ses contributeur·ices.", | |||
"de": " und ihren Kontributor*innen" | |||
}, | |||
"index_credits_desc2": { | |||
"en": "The Simple Nextcloud Forms software, which simplifies the form creation process, has been developed by ", | |||
"fr": "Le logiciel Simple Nextcloud Forms, qui simplifie la création de formulaires, a été développé par ", | |||
"de": "Die Simple Nextcloud Forms Software, welche die Erstellung von Umfragen erleichtert, wurde entwickelt von " | |||
}, | |||
"index_credits_desc2_for": { | |||
"en": " for ", | |||
"fr": " pour ", | |||
"de": " für " | |||
}, | |||
"index_credits_desc2_org": { | |||
"en": "the 42l association", | |||
"fr": "l'association 42l", | |||
"de": "die 42l Assoziation" | |||
}, | |||
"index_credits_desc3": { | |||
"en": "source code", | |||
"fr": "code source", | |||
"de": "Quellcode" | |||
}, | |||
"link_title": { | |||
"en": "Link created", | |||
"fr": "Lien créé", | |||
"de": "Link erstellt" | |||
}, | |||
"link_desc1_1": { | |||
"en": "Here's an <b>administration link</b>, which will allow you to access all", | |||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous", | |||
"de": "Hier ist ein <b>Administrations Link</b>, der es ermöglicht wieder zu" | |||
}, | |||
"link_desc1_2": { | |||
"en": "your forms and check your answers.", | |||
"fr": "vos formulaires et de consulter vos réponses.", | |||
"de": "ihren Umfragen zu gelangen und die Antworten einzusehen." | |||
}, | |||
"link_desc2_1": { | |||
"en": "<b>Keep it</b> carefully and don't give it away", | |||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas", | |||
"de": "<b>Bewahren Sie diese</b> gut und sicher auf" | |||
}, | |||
"link_desc2_2": { | |||
"en": "(it'd be the same as giving out your password!).", | |||
"fr": "(cela reviendrait à donner un mot de passe!).", | |||
"de": "(Die Weitergabe entspricht der Weitergabe eines Passwortes!)." | |||
}, | |||
"link_desc3_1": { | |||
"en": "Once your link copied, click on the button below to", | |||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour", | |||
"de": "Ist der Link kopiert, drücken sie auf den unteren Button um" | |||
}, | |||
"link_desc3_2": { | |||
"en": "start editing your forms.", | |||
"fr": "commencer à éditer vos formulaires.", | |||
"de": "Umfragen zu erstellen oder zu bearbeiten." | |||
}, | |||
"link_access_btn": { | |||
"en": "to foorms", | |||
"fr": "Accéder foorms", | |||
"de": "zu foorms" | |||
}, | |||
"link_note": { | |||
"en": "Note: If you don't use your administration link during more than ", | |||
"fr": "Note : Si vous n'utilisez pas votre lien d'administration pendant plus de ", | |||
"de": "Notiz: Wenn Sie den Administrations Link für länger als " | |||
}, | |||
"link_note2": { | |||
"en": " days, your forms will be automatically deleted.", | |||
"fr": " jours, vos formulaires seront automatiquement supprimés.", | |||
"de": " Tage nicht benutzen, werden ihre Umfragen automatisch gelöscht." | |||
}, | |||
"link_copy": { | |||
"en": "Copy link", | |||
"fr": "Copier le lien", | |||
"de": "Link kopieren" | |||
}, | |||
"link_copied": { | |||
"en": "Link copied!", | |||
"fr": "Lien copié !", | |||
"de": "Link kopiert !" | |||
}, | |||
"link_mail": { | |||
"en": "send Link", | |||
"fr": "envoyer lien", | |||
"de": "Link senden" | |||
}, | |||
"error_title": { | |||
"en": "Oops!...", | |||
"fr": "Oups !...", | |||
"de": "Ups !..." | |||
}, | |||
"error_description": { | |||
"en": "The application encountered a problem:", | |||
"fr": "L'application a rencontré un problème :", | |||
"de": "Die Anwendung hat ein Problem festgestellt:" | |||
}, | |||
"error_back": { | |||
"en": "Back to the main page", | |||
"fr": "Retour à la page principale", | |||
"de": "Zurück zur Hauptseite" | |||
}, | |||
"error_note1": { | |||
"en": "We are (probably) aware of this bug, but feel free to contact us if you need assistance.", | |||
"fr": "Nous sommes (probablement) au courant, mais n'hésitez pas à nous contacter si vous avez besoin d'aide.", | |||
"de": "Wir sind uns (wahrscheinlich) bewusst, was diesen Fehler angeht. Fühlen sie sich frei uns zu kontaktieren, wenn Sie Hilfe benötigen." | |||
}, | |||
"error_note2": { | |||
"en": "Sorry for the inconvenience.", | |||
"fr": "Désolés pour les désagréments occasionnés.", | |||
"de": "Entschuldigen Sie die Störung." | |||
}, | |||
"error_forward_req": { | |||
"en": "Error while connecting to the Nextcloud instance.", | |||
"fr": "Erreur lors de la connexion à l'instance Nextcloud.", | |||
"de": "Fehler beim Verbinden zur Nextcloud Instanz." | |||
}, | |||
"error_forward_resp": { | |||
"en": "Error while reading Nextcloud instance's response.", | |||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud.", | |||
"de": "Feher beim Lesen der Antwort der Nextcloud Instanz." | |||
}, | |||
"error_forward_isanon": { | |||
"en": "Couldn't set the form's isAnonymous value.", | |||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire.", | |||
"de": "Es ist nicht möglich, die isAnonymous Wert des Formulars zu setzen." | |||
}, | |||
"error_forward_clientresp_newform": { | |||
"en": "Failed to send the response body (new form).", | |||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire).", | |||
"de": "Fehler beim senden des Response body (neues Formular)." | |||
}, | |||
"error_forward_clientresp_std": { | |||
"en": "Failed to send the response body.", | |||
"fr": "Échec lors de l'envoi du corps de la réponse.", | |||
"de": "Fehler beim Senden des Response Body." | |||
}, | |||
"error_forwardlogin_db": { | |||
"en": "Couldn't connect to the local database.", | |||
"fr": "Échec lors de la connexion à la base de données locale.", | |||
"de": "Fehler beim verbinden zur lokalen Datenbank." | |||
}, | |||
"error_forwardlogin_db_get": { | |||
"en": "Error during information retrieval from the local database.", | |||
"fr": "Erreur lors de la récupération des informations dans la base de données locale.", | |||
"de": "Fehler beim Empfangen von Daten der lokalen Datenbank." | |||
}, | |||
"error_forwardlogin_notfound": { | |||
"en": "The specified token doesn't exist in local database.", | |||
"fr": "Le token spécifié n'existe pas dans la base de données locale.", | |||
"de": "Der gesetzte Token existiert nicht in der lokalen Datenbank." | |||
}, | |||
"error_login_get": { | |||
"en": "The account creation request (GET) to Nextcloud has failed.", | |||
"fr": "La requête de création de compte (GET) vers l'instance Nextcloud a échoué.", | |||
"de": "Das Account Erstellungs Request (GET) zu Nextcloud hat nicht funktioniert." | |||
}, | |||
"error_login_get_body": { | |||
"en": "Reading response from the account creation request to Nextcloud has failed.", | |||
"fr": "La lecture de la réponse à la requête de création de compte vers l'instance Nextcloud a échoué.", | |||
"de": "Das Lesen der Response vom Account Erstellungs Request zu Nextcloud hat nicht funktioniert." | |||
}, | |||
"error_login_post": { | |||
"en": "The account creation request (POST) to Nextcloud has failed.", | |||
"fr": "La requête de création de compte (POST) vers l'instance Nextcloud a échoué.", | |||
"de": "Der Account Erstellungs Request (POST) zu Nextcloud hat nicht funktioniert. " | |||
}, | |||
"error_login_redir": { | |||
"en": "Redirection to Nextcloud account failed.", | |||
"fr": "La redirection vers le compte Nextcloud a échoué.", | |||
"de": "Die Weiterleitung zum Nextcloud account hat nicht funktioniert." | |||
}, | |||
"error_createaccount_post": { | |||
"en": "Account creation: connection to the Nextcloud API failed.", | |||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué.", | |||
"de": "Account Erstellung: Verbindung zur Nextcloud API hat nicht funktioniert." | |||
}, | |||
"error_createaccount_post_body": { | |||
"en": "Account creation: reading the answer from the Nextcloud API failed.", | |||
"fr": "Création de compte : le traitement de la réponse de l'API Nextcloud a échoué.", | |||
"de": "Account Erstellung : das Lesen der Antwort der Nextcloud API hat nicht funktioniert." | |||
}, | |||
"error_createaccount_status": { | |||
"en": "The Nextcloud instance responded with an unexpected status code.", | |||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu.", | |||
"de": "Die Nextcloud Instanz hat mit einem unexpected status code geantwortet." | |||
}, | |||
"error_createaccount_ncstatus": { | |||
"en": "The Nextcloud API responded with an unexpected status code.", | |||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu.", | |||
"de": "Die Nextcloud API hat mit unexpected ncstatus geantwortet." | |||
}, | |||
"error_createaccount_ncstatus_parse": { | |||
"en": "Error parsing Nextcloud API's status code.", | |||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud.", | |||
"de": "Fehler beim Lesen des Nextcloud API status codes." | |||
}, | |||
"error_forwardregister_pool": { | |||
"en": "Error while connecting to the local database.", | |||
"fr": "Erreur lors de la connexion à la base de données locale.", | |||
"de": "Fehler beim Verbinden zu der lokalen Datenbank." | |||
}, | |||
"error_forwardregister_db": { | |||
"en": "Failed adding the Nextcloud account in the local database.", | |||
"fr": "L'ajout du compte Nextcloud dans la base de données locale a échoué.", | |||
"de": "Fehlre beim Hinzufügen des Nextcloud Accounts zur lokalen Datenbank." | |||
}, | |||
"error_forwardregister_tokenparse": { | |||
"en": "Failed parsing the admin token.", | |||
"fr": "Échec lors de la lecture du token administrateur.", | |||
"de": "Fehler beim Parsen des Admin Tokens." | |||
}, | |||
"error_login_cookiepair": { | |||
"en": "Couldn't read cookies.", | |||
"fr": "Échec lors de la lecture de cookies.", | |||
"de": "Fehler beim Lesen der Cookies" | |||
}, | |||
"error_login_regex": { | |||
"en": "Couldn't read the CSRF token.", | |||
"fr": "Échec lors de la lecture du token CSRF.", | |||
"de": "Fehler beim Lesen des CSRF Tokens." | |||
}, | |||
"error_login_setcookie": { | |||
"en": "Error during cookies transfer.", | |||
"fr": "Erreur lors du transfert de cookies.", | |||
"de": "Feheler beim Transfer der Cookies." | |||
}, | |||
"error_form_insert": { | |||
"en": "The local database couldn't be reached.", | |||
"fr": "Échec de la connexion avec la base de données locale.", | |||
"de": "Die lokale Datenbank ist nicht erreichbar." | |||
}, | |||
"error_createaccount": { | |||
"en": "The Nextcloud API returned an unexpected result.", | |||
"fr": "L'API de Nextcloud a retourné un résultat inattendu.", | |||
"de": "Die Nextcloud API hat ein unerwartetes Resultat zurückgesendet." | |||
}, | |||
"error_redirect": { | |||
"en": "Failed to redirect.", | |||
"fr": "La redirection a échoué.", | |||
"de": "Weiterleitung (Redirect) hat nicht funktioniert." | |||
}, | |||
"error_csrf_cookie": { | |||
"en": "Your CSRF token (cookie) seems incorrect, please retry.", | |||
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer.", | |||
"de": "Dein CSRF Token (Cookie) scheint inkorrekt, versuchen Sie es erneut." | |||
}, | |||
"error_csrf_token": { | |||
"en": "Your CSRF token seems incorrect, please retry.", | |||
"fr": "Votre token CSRF semble incorrect, veuillez réessayer.", | |||
"de": "Ihr CSRF Token scheint nicht korrekt, versuchen Sie es erneut. " | |||
}, | |||
"error_dirtyhacker": { | |||
"en": "Attempt to access an unauthorized resource.", | |||
"fr": "Tentative d'accès à une ressource non autorisée.", | |||
"de": "Zugangs-Versuch einer unauthorisierten Quelle." | |||
}, | |||
"error_tplrender": { | |||
"en": "Template rendering failed.", | |||
"fr": "Le rendu du template a échoué.", | |||
"de": "Template rendering hat nicht funktioniert." | |||
}, | |||
"error_tplrender_resp": { | |||
"en": "Sending response failed.", | |||
"fr": "L'envoi de la réponse a échoué.", | |||
"de": "Senden der Antwort hat nicht funktioniert." | |||
} | |||
} | |||
@ -0,0 +1,305 @@ | |||
<!DOCTYPE html> | |||
<html lang="it"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
<title>{{ "link_title"|tr(lang) }} – {{ "index_title"|tr(lang) }}</title> | |||
<meta name="robots" content="noindex" /> | |||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" /> | |||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.svg" /> | |||
<link rel="stylesheet" href="/assets/index.css?v=1.0" /> | |||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" /> | |||
<link rel="stylesheet" href="/assets/digitalcourage.css" /> | |||
<link rel="stylesheet" href="/assets/bootstrap.min.css" /> | |||
<script type="text/javascript"> | |||
window.onload = function () { | |||
// show link copy button if javascript is enabled | |||
document.getElementById("script-copy").style.display = "unset"; | |||
let btn = document.getElementById("script-copy-btn"); | |||
btn.style.cursor = "pointer"; | |||
let csrf_token = "{{ csrf_token }}"; | |||
let lang = "{{ lang }}"; | |||
document.getElementById('langs').value=lang; | |||
document.getElementById('new_link_button').addEventListener('click', function () { | |||
new_link(csrf_token); | |||
}); | |||
btn.addEventListener('click', function() { | |||
var copyText = document.getElementById("link"); | |||
/* Select the text field */ | |||
copyText.select(); | |||
copyText.setSelectionRange(0, 99999); | |||
document.execCommand("copy"); | |||
btn.innerHTML = '{{ "link_copied"|tr(lang) }}'; | |||
}); | |||
function ValidateEmail(mail) | |||
{ | |||
if (/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(mail)) | |||
{ | |||
return (true) | |||
} | |||
alert("Die eingegebene Mail Adresse ist ungültig. Sie können sich auch anmelden, ohne den Token zugeschickt zu bekommen.") | |||
return (false) | |||
} | |||
document.getElementById("email-register").style.display = "unset"; | |||
let btn2 = document.getElementById("email-register-btn"); | |||
btn2.style.cursor = "pointer"; | |||
btn2.addEventListener('click', function() { | |||
var email = document.getElementById("email").value; | |||
var adtok = document.getElementById("link").value; | |||
console.log(email); | |||
var validation = ValidateEmail(email); | |||
/* var emailjsonstring = JSON.stringify(JSON.parse(document.getElementById('email'))); */ | |||
if (validation == true) | |||
{ | |||
var xhr1=new XMLHttpRequest(); | |||
xhr1.open("POST",'link/email', true); | |||
xhr1.send(email + ',' + adtok + '\n'); | |||
document.getElementById("email").value = "Die Email ist auf dem Weg"; | |||
} | |||
}); | |||
} | |||
function new_link(csrf) { | |||
var sel = document.getElementById('langs'); | |||
let opt = getSelectedOption(sel); | |||
let lang = opt.value; | |||
document.getElementById('langs').value = lang; | |||
document.getElementById("link_lang").value = lang; | |||
document.getElementById("csrf_token").value = csrf; | |||
document.getElementById('new_link').submit(); | |||
document.getElementById('new_link_button').classList.add("hidden"); | |||
document.getElementById('loading_ring').classList.remove("hidden"); | |||
} | |||
function getSelectedOption(sel) { | |||
var opt; | |||
for ( var i = 0, len = sel.options.length; i < len; i++ ) { | |||
opt = sel.options[i]; | |||
if ( opt.selected === true ) { | |||
break; | |||
} | |||
} | |||
return opt; | |||
} | |||
</script> | |||
<style> | |||
.break { | |||
flex-basis: 100%; | |||
height: 0; | |||
} | |||
.div_120 { | |||
flex-basis: 100%; | |||
height: 120px; | |||
} | |||
.div_45 { | |||
flex-basis: 100%; | |||
height: 45px; | |||
} | |||
.div_35 { | |||
flex-basis: 100%; | |||
height: 35px; | |||
} | |||
.div_25 { | |||
flex-basis: 100%; | |||
height: 25px; | |||
} | |||
.div_10 { | |||
flex-basis: 100%; | |||
height: 10px; | |||
} | |||
.grid-container { | |||
display: grid; | |||
grid-template-columns: auto auto; | |||
width: 725px; | |||
grid-gap: 25px; | |||
} | |||
.grid-container2 { | |||
display: grid; | |||
grid-template-columns: auto auto auto; | |||
width: 532px; | |||
grid-gap: 20px; | |||
} | |||
@media only screen and (max-width: 768px) { | |||
/* For mobile phones: */ | |||
[class*="grid-container"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
justify-content: center; | |||
} | |||
[class*="grid-container2"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
justify-content: center; | |||
} | |||
[class*="item2"] { | |||
grid-template-columns: auto; | |||
max-width: 100%; | |||
align-items: center; | |||
} | |||
} | |||
.center { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.item1 { | |||
width: 350px; | |||
height: 200px; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.item2 { | |||
width: 350px; | |||
height: 200px; | |||
#display: flex; | |||
#justify-content: center; | |||
#align-items: center; | |||
} | |||
.h2 { | |||
font-size: 30pt; | |||
} | |||
p { | |||
font-size: 16pt; | |||
} | |||
.downDC { | |||
height: 90px; | |||
padding: 10px; | |||
} | |||
.c-img-shadow { | |||
height: 200px; | |||
max-width: 100%; | |||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.18),0 5px 5px rgba(0, 0, 0, 0.18); | |||
border-radius: 2px; | |||
} | |||
</style> | |||
<body> | |||
<div class="container ombre"> | |||
<header role="banner" class="clearfix"> | |||
<form id="new_link" method="post" action="/link" class="hidden-print"> | |||
<div class="input-group input-group-sm pull-right col-xs-12 col-sm-2 langs" style="margin-right: 8px"> | |||
<select id="langs" name="lang" class="form-control" title="Select language" > | |||
<option lang="fr" value="fr">Français</option> | |||
<option lang="en" selected value="en">English</option> | |||
<option lang="oc" value="oc">Occitan</option> | |||
<option lang="es" value="es">Español</option> | |||
<option lang="de" value="de">Deutsch</option> | |||
<option lang="nl" value="nl">Dutch</option> | |||
<option lang="it" value="it">Italiano</option> | |||
<option lang="br" value="br">Brezhoneg</option> | |||
</select> | |||
<input id="csrf_token" name="csrf_token" type="text" class="hidden"> | |||
<input id="link_lang" name="link_lang" type="text" class="hidden"> | |||
<span class="input-group-btn"> | |||
<a id="new_link_button" class="btn btn-default btn-sm language_button" title="Change language">OK</a> | |||
</span> | |||
</div> | |||
</form> | |||
<a href="https://foorms.digitalcourage.de/" title="Home - foorms" style="margin-left: 8px" > | |||
<img src="/assets/foorms_logo_beta.svg" alt="foorms" class="" height="58vh" /> | |||
</a> | |||
<h2 class="lead col-xs-12"></h2> <div class="trait col-xs-12" role="presentation"></div> | |||
</header> | |||
<main role="main"> | |||
<center> | |||
<div class="has-text-centered"> | |||
<br /> | |||
<h2>{{ "link_title"|tr(lang) }}</h2> | |||
<div class="div_25"> </div> | |||
<p>{{ "link_desc1_1"|tr(lang)|safe }}</p> | |||
<div class="break"> </div> | |||
<p>{{ "link_desc1_2"|tr(lang)|safe }}</p> | |||
<div class="div_25"> </div> | |||
<div class="c-flex c-jumbo"> | |||
<input id="link" class="ncstyle-input" type="text" style='font-size: 16px; text-align:center' size="80" readonly value="{{ config.sncf_url }}/admin/{{ admin_token }}" /> | |||
</div> | |||
<div class="div_35"> </div> | |||
<div id="script-copy"> | |||
<div class="c-flex"> | |||
<a id="script-copy-btn" class="ncstyle-button margin-bottom">{{ "link_copy"|tr(lang) }}</a> | |||
</div> | |||
</div> | |||
<div class="div_120"> </div> | |||
<p>{{ "link_desc2_1"|tr(lang)|safe }}</p> | |||
<div class="break"> </div> | |||
<p>{{ "link_desc2_2"|tr(lang)|safe }}</p> | |||
<div class="div_25"> </div> | |||
<div class="c-flex"> | |||
<input id="email" class="ncstyle-input" style="text-align:center;" type="text" value="Send_Password_Link@invalid" /> | |||
</div> | |||
<div class="div_35"> </div> | |||
<div id="email-register"> | |||
<div class="c-flex"> | |||
<a id="email-register-btn" class="ncstyle-button margin-bottom">{{ "link_mail"|tr(lang) }}</a> | |||
</div> | |||
<div class="div_120"> </div> | |||
</div> | |||
<p>{{ "link_desc3_1"|tr(lang) }}</p> | |||
<div class="break"></div> | |||
<p>{{ "link_desc3_2"|tr(lang) }}</p> | |||
<div class=div_35></div> | |||
<div class="c-flex"> | |||
<a id="forms-btn" class="ncstyle-button margin-bottom" href="{{ config.sncf_url }}/admin/{{ admin_token }}">{{ "link_access_btn"|tr(lang) }}</a> | |||
</div> | |||
</div> | |||
<div class="div_120"></div> | |||
</center> | |||
<center> | |||
<div class="c-blue grid-container2"> | |||
<a href="https://42l.fr/Rapport-technique" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a> | |||
<a href="https://git.42l.fr/neil/sncf" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a> | |||
<a href="https://git.42l.fr/neil/sncf/src/branch/root/LICENSE" style="font-size:15px;" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a> | |||
</div> | |||
</center> | |||
<div class="div_10"></div> | |||
<div class="div_10"></div> | |||
<div class="div_10"></div> | |||
</main> | |||
</div> <!-- .container --> | |||
<div class="container ombre downDC" style="display:flex; align-items:center;"> | |||
<h2 class="lead"><a target="_blank" href="https://digitalcourage.de/">Digitalcourage</a> | <a target="_blank" href="https://digitalcourage.de/newsletter">Newsletter</a> | <a target="_blank" href="https://digitalcourage.de/spenden">{{ "impressum_donations"|tr(lang)|safe }}</a> | <a target="_blank" href="https://digitalcourage.de/en">Impressum</a> | <a target="_blank" href="https://digitalcourage.de/en">{{ "impressum_privacy"|tr(lang)|safe }}</a> </h2> | |||
</div> | |||
</body> | |||
</html> | |||
@ -0,0 +1,104 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
#[macro_use] | |||
extern crate diesel; | |||
#[macro_use] | |||
extern crate diesel_migrations; | |||
use actix_session::CookieSession; | |||
use actix_web::cookie::SameSite; | |||
use actix_files::Files; | |||
use actix_web::client::Client; | |||
use actix_web::{web, App, FromRequest, HttpServer}; | |||
use diesel::prelude::*; | |||
use diesel::r2d2::{self, ConnectionManager}; | |||
use url::Url; | |||
use crate::config::CONFIG; | |||
use crate::config::PAYLOAD_LIMIT; | |||
use crate::forward::*; | |||
mod account; | |||
mod config; | |||
mod database; | |||
mod errors; | |||
mod forward; | |||
mod sniff; | |||
mod templates; | |||
// default to postgres | |||
#[cfg(feature = "default")] | |||
type DbConn = PgConnection; | |||
#[cfg(feature = "default")] | |||
embed_migrations!("migrations/postgres"); | |||
#[cfg(feature = "postgres")] | |||
type DbConn = PgConnection; | |||
#[cfg(feature = "postgres")] | |||
embed_migrations!("migrations/postgres"); | |||
#[cfg(feature = "sqlite")] | |||
type DbConn = SqliteConnection; | |||
#[cfg(feature = "sqlite")] | |||
embed_migrations!("migrations/sqlite"); | |||
#[cfg(feature = "mysql")] | |||
type DbConn = MysqlConnection; | |||
#[cfg(feature = "mysql")] | |||
embed_migrations!("migrations/mysql"); | |||
type DbPool = r2d2::Pool<ConnectionManager<DbConn>>; | |||
#[actix_web::main] | |||
async fn main() -> std::io::Result<()> { | |||
/* std::env::set_var("RUST_LOG", "actix_web=debug"); | |||
env_logger::init();*/ | |||
println!("ta ta tala ~ SNCF init"); | |||
println!("Checking configuration file..."); | |||
CONFIG.check_version(); | |||
if CONFIG.database_path.is_empty() { | |||
println!("No database specified. Please enter a MySQL, PostgreSQL or SQLite connection string in config.toml."); | |||
} | |||
debug(&format!("Opening database {}", CONFIG.database_path)); | |||
let manager = ConnectionManager::<DbConn>::new(&CONFIG.database_path); | |||
let pool = r2d2::Pool::builder() | |||
.build(manager) | |||
.expect("ERROR: main: Failed to create the database pool."); | |||
let conn = pool.get().expect("ERROR: main: DB connection failed"); | |||
println!("Running migrations..."); | |||
embedded_migrations::run(&*conn).expect("ERROR: main: Failed to run database migrations"); | |||
let forward_url = | |||
Url::parse(&CONFIG.nextcloud_url).expect("Couldn't parse the forward url from config"); | |||
println!( | |||
"Now listening at {}:{}", | |||
CONFIG.listening_address, CONFIG.listening_port | |||
); | |||
// starting the http server | |||
HttpServer::new(move || { | |||
App::new() | |||
.data(pool.clone()) | |||
.data(Client::new()) | |||
.data(forward_url.clone()) | |||
.wrap( | |||
CookieSession::signed(&[0; 32]) | |||
.secure(true) | |||
.same_site(SameSite::Strict) | |||
.http_only(true) | |||
.name("sncf_cookies") | |||
) | |||
/*.route("/mimolette", web::get().to(login))*/ | |||
/*.route("/login", web::post().to(forward))*/ | |||
/*.wrap(middleware::Compress::default())*/ | |||
.service(Files::new("/assets/", "./templates/assets/").index_file("index.html")) | |||
.route("/", web::get().to(index)) | |||
.route("/link", web::post().to(forward_register)) | |||
.route("/admin/{token}", web::get().to(forward_login)) | |||
.default_service(web::route().to(forward)) | |||
.data(String::configure(|cfg| cfg.limit(PAYLOAD_LIMIT))) | |||
.app_data(actix_web::web::Bytes::configure(|cfg| { | |||
cfg.limit(PAYLOAD_LIMIT) | |||
})) | |||
}) | |||
.bind((CONFIG.listening_address.as_str(), CONFIG.listening_port))? | |||
.system_exit() | |||
.run() | |||
.await | |||
} | |||
pub fn debug(text: &str) { | |||
if CONFIG.debug_mode { | |||
println!("{}", text); | |||
} | |||
} |
@ -0,0 +1,76 @@ | |||
use serde_json::Value; | |||
use std::fs::File; | |||
use std::io::Read; | |||
use std::io::{self, BufRead, BufReader}; | |||
use std::path::Path; | |||
// payload limit set to 5MiB | |||
pub const PAYLOAD_LIMIT: usize = 10_000_000; | |||
pub const PROXY_TIMEOUT: u64 = 15; | |||
pub const CONFIG_FILE: &str = "./config.toml"; | |||
pub const CONFIG_VERSION: u8 = 2; | |||
pub const ADJ_LIST_FILE: &str = "./adj-list.txt"; | |||
pub const NAME_LIST_FILE: &str = "./name-list.txt"; | |||
pub const LOC_FILE: &str = "./lang.json"; | |||
pub const USER_AGENT: &str = "Actix-web"; | |||
lazy_static! { | |||
pub static ref CONFIG: Config = Config::init(); | |||
pub static ref ADJ_LIST: Vec<String> = | |||
lines_from_file(ADJ_LIST_FILE).expect("Failed to load adjectives list"); | |||
pub static ref NAME_LIST: Vec<String> = | |||
lines_from_file(NAME_LIST_FILE).expect("Failed to load names list"); | |||
pub static ref LOC: Value = init_lang(); | |||
} | |||
// Open LOC_FILE and store it in memory (LOC) | |||
fn init_lang() -> Value { | |||
let mut file = File::open(LOC_FILE).expect("init_lang: Can't open translations file"); | |||
let mut data = String::new(); | |||
file.read_to_string(&mut data) | |||
.expect("init_lang: Can't read translations file"); | |||
serde_json::from_str(&data).expect("init_lang(): Can't parse translations file") | |||
} | |||
// Open a file from its path | |||
fn lines_from_file(filename: impl AsRef<Path>) -> io::Result<Vec<String>> { | |||
BufReader::new(File::open(filename)?).lines().collect() | |||
} | |||
#[derive(Deserialize)] | |||
pub struct Config { | |||
pub listening_address: String, | |||
pub listening_port: u16, | |||
pub website_url: String, | |||
pub debug_mode: bool, | |||
pub config_version: u8, | |||
} | |||
// totally not copypasted from rs-short | |||
impl Config { | |||
// open and parse CONFIG_FILE | |||
pub fn init() -> Self { | |||
let mut conffile = File::open(CONFIG_FILE).expect( | |||
r#"Config file config.toml not found. | |||
Please create it using config.toml.sample."#, | |||
); | |||
let mut confstr = String::new(); | |||
conffile | |||
.read_to_string(&mut confstr) | |||
.expect("Couldn't read config to string"); | |||
toml::from_str(&confstr).expect("Couldn't deserialize the config. Please update at https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version --- Error") | |||
} | |||
// if config.config_version doesn't match the hardcoded version, | |||
// ask the admin to manually upgrade its config file | |||
pub fn check_version(&self) { | |||
if self.config_version != CONFIG_VERSION { | |||
eprintln!("Your configuration file is obsolete!\nPlease update it following the instructions in https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version and update its version to {}.", CONFIG_VERSION); | |||
panic!(); | |||
} | |||
} | |||
} | |||
@ -0,0 +1,58 @@ | |||
use crate::templates::TplError; | |||
use actix_web::dev::HttpResponseBuilder; | |||
use actix_web::{error, http::header, http::StatusCode, HttpResponse}; | |||
use askama::Template; | |||
use std::fmt; | |||
pub fn crash(lang: String, error_msg: &'static str) -> TrainCrash { | |||
TrainCrash { lang, error_msg } | |||
} | |||
#[derive(Debug)] | |||
pub struct TrainCrash { | |||
pub error_msg: &'static str, | |||
pub lang: String, | |||
} | |||
// gonna avoid using failure crate | |||
// by implementing display | |||
impl fmt::Display for TrainCrash { | |||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |||
write!(f, "{:?}", self.error_msg) | |||
} | |||
} | |||
impl error::ResponseError for TrainCrash { | |||
fn error_response(&self) -> HttpResponse { | |||
eprintln!("Error reached: {}", self.error_msg); | |||
HttpResponseBuilder::new(self.status_code()) | |||
.set_header(header::CONTENT_TYPE, "text/html; charset=utf-8") | |||
.body( | |||
TplError { | |||
lang: &self.lang, | |||
error_msg: self.error_msg, | |||
} | |||
.render() | |||
.expect("error_tplrender (TplError). Empty page sent to client."), | |||
) | |||
} | |||
fn status_code(&self) -> StatusCode { | |||
match self.error_msg { | |||
"error_forward_req" => StatusCode::BAD_GATEWAY, | |||
"error_forward_resp" => StatusCode::BAD_GATEWAY, | |||
"error_login_get" => StatusCode::BAD_GATEWAY, | |||
"error_login_get_body" => StatusCode::BAD_GATEWAY, | |||
"error_login_post" => StatusCode::BAD_GATEWAY, | |||
"error_login_redir" => StatusCode::BAD_GATEWAY, | |||
"error_forwardlogin_notfound" => StatusCode::NOT_FOUND, | |||
"error_forwardregister_tokenparse" => StatusCode::BAD_REQUEST, | |||
"error_login_cookiepair" => StatusCode::BAD_GATEWAY, | |||
"error_login_regex" => StatusCode::BAD_GATEWAY, | |||
"error_login_setcookie" => StatusCode::BAD_REQUEST, | |||
"error_createaccount" => StatusCode::BAD_GATEWAY, | |||
"error_dirtyhacker" => StatusCode::UNAUTHORIZED, | |||
_ => StatusCode::INTERNAL_SERVER_ERROR, | |||
} | |||
} | |||
} |
@ -0,0 +1,147 @@ | |||
use actix_web::client::{Client, ClientRequest}; | |||
use actix_web::{http, web, HttpRequest, HttpResponse}; | |||
use actix_session::Session; | |||
use askama::Template; | |||
use chrono::Utc; | |||
use std::time::Duration; | |||
use url::Url; | |||
use crate::config::PAYLOAD_LIMIT; | |||
use crate::config::PROXY_TIMEOUT; | |||
use crate::debug; | |||
use crate::errors::{crash, TrainCrash}; | |||
use crate::sniff::*; | |||
use crate::templates::*; | |||
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/text" { | |||
//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("tuples.csv") | |||
.expect("Unable to open file"); | |||
////f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data"); | |||
f.write_all(&body).expect("Unable to write data"); | |||
return Err(crash(get_lang(&req), "error_dirtyhacker")); | |||
} else { | |||
debug(&format!("Restricted route blocked: {}", route)); | |||
return Ok(web_redir("/").await.map_err(|e| { | |||
eprintln!("error_redirect: {}", e); | |||
crash(get_lang(&req), "error_redirect") | |||
})?); | |||
} | |||
} | |||
#[derive(Deserialize)] | |||
pub struct CsrfToken { | |||
pub link_lang: String, | |||
} | |||
// 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>, | |||
) -> Result<HttpResponse, TrainCrash> { | |||
let lang = csrf_post.link_lang.clone(); | |||
Ok(HttpResponse::Ok() | |||
.content_type("text/html") | |||
.body( | |||
TplLink { | |||
lang: &lang, | |||
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<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> { | |||
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") | |||
})?) | |||
} | |||
@ -0,0 +1,69 @@ | |||
#[macro_use] | |||
extern crate lazy_static; | |||
#[macro_use] | |||
extern crate serde_derive; | |||
#[macro_use] | |||
extern crate diesel; | |||
#[macro_use] | |||
extern crate diesel_migrations; | |||
use actix_session::CookieSession; | |||
use actix_web::cookie::SameSite; | |||
use actix_files::Files; | |||
use actix_web::client::Client; | |||
use actix_web::{web, App, FromRequest, HttpServer}; | |||
use diesel::prelude::*; | |||
use url::Url; | |||
use crate::config::CONFIG; | |||
use crate::config::PAYLOAD_LIMIT; | |||
use crate::forward::*; | |||
mod config; | |||
mod errors; | |||
mod forward; | |||
mod sniff; | |||
mod templates; | |||
#[actix_web::main] | |||
async fn main() -> std::io::Result<()> { | |||
/* std::env::set_var("RUST_LOG", "actix_web=debug"); | |||
env_logger::init();*/ | |||
println!("ta ta tala ~ SNCF init"); | |||
println!("Checking configuration file..."); | |||
CONFIG.check_version(); | |||
println!( | |||
"Now listening at {}:{}", | |||
CONFIG.listening_address, CONFIG.listening_port | |||
); | |||
// starting the http server | |||
HttpServer::new(move || { | |||
App::new() | |||
.data(Client::new()) | |||
.data(forward_url.clone()) | |||
//.wrap( | |||
// CookieSession::signed(&[0; 32]) | |||
// .secure(true) | |||
// .same_site(SameSite::Strict) | |||
// .http_only(true) | |||
// .name("pluriton_cookies") | |||
// ) | |||
.service(Files::new("/assets/", "./templates/assets/").index_file("index.html")) | |||
.route("/", web::get().to(index)) | |||
.route("/link/text", web::post().to(forward_register)) | |||
.default_service(web::route().to(forward)) | |||
.data(String::configure(|cfg| cfg.limit(PAYLOAD_LIMIT))) | |||
.app_data(actix_web::web::Bytes::configure(|cfg| { | |||
cfg.limit(PAYLOAD_LIMIT) | |||
})) | |||
}) | |||
.bind((CONFIG.listening_address.as_str(), CONFIG.listening_port))? | |||
.system_exit() | |||
.run() | |||
.await | |||
} | |||
pub fn debug(text: &str) { | |||
if CONFIG.debug_mode { | |||
println!("{}", text); | |||
} | |||
} |
@ -0,0 +1,101 @@ | |||
use actix_web::web; | |||
use serde_json::Value; | |||
use crate::debug; | |||
// checks to be done on user requests | |||
// if it returns true, cancels the request | |||
pub fn check_request(route: &str, body: &web::Bytes) -> bool { | |||
match route { | |||
"/ocs/v2.php/apps/forms/api/v1/form/update" => rq_form_update(body), | |||
_ => false, | |||
} | |||
} | |||
// prevents the user from doing anything other than link sharing. | |||
fn rq_form_update(body: &web::Bytes) -> bool { | |||
let req = String::from_utf8_lossy(body); | |||
// try to serialize the body. | |||
// If the parsing fails, drop the request | |||
let v: Value = serde_json::from_str(&req).unwrap_or_else(|e| { | |||
eprintln!("check_request: failed to parse JSON: {}", e); | |||
Value::Null | |||
}); | |||
// if the type or isAnonymous is set (isn't null), | |||
// drop the request. | |||
// Also drop if v is null because of parsing fail. | |||
v == Value::Null | |||
|| v["keyValuePairs"]["isAnonymous"] != Value::Null | |||
|| v["keyValuePairs"]["access"]["type"] != Value::Null | |||
} | |||
// checks to be done on responses from the Nextcloud instance | |||
// if it returns true, cancels the request | |||
// NOTE: unused for now | |||
/*pub fn check_response(_route: &str, _body: &web::Bytes) -> bool { | |||
false | |||
}*/ | |||
// checks if a form has been created. | |||
// if it's the case, sets some parameters. | |||
// this part may need code quality improvements | |||
// the body MUST come from the "create new form" route | |||
// (this is checked upstream) | |||
// returns the form UID and the request body | |||
pub fn check_new_form(body: &web::Bytes) -> u64 { | |||
let req = String::from_utf8_lossy(body); | |||
// finds the form ID | |||
let v: Value = serde_json::from_str(&req).unwrap_or_else(|e| { | |||
eprintln!("check_new_form: failed to parse JSON: {}", e); | |||
Value::Null | |||
}); | |||
if v != Value::Null | |||
&& v["ocs"].is_object() | |||
&& v["ocs"]["data"].is_object() | |||
&& v["ocs"]["data"]["id"] != Value::Null | |||
&& v["ocs"]["data"]["isAnonymous"] == Value::Null | |||
{ | |||
//getting form id | |||
v["ocs"]["data"]["id"].as_u64().unwrap_or_else(|| { | |||
eprintln!("check_new_form: failed to parse formid: {}", v); | |||
0 | |||
}) | |||
} else { | |||
eprintln!("error: check_new_form: can't find formid: {}", v); | |||
0 | |||
} | |||
} | |||
// those routes won't be redirected | |||
const BLOCKED_ROUTES: &[&str] = &[ | |||
"/apps/settings", | |||
"/login", | |||
"/settings", | |||
"/ocs/v", | |||
"/remote.php", | |||
"/core/templates/filepicker.html", | |||
]; | |||
// ...except if they are in this list | |||
const ALLOWED_ROUTES: &[&str] = &["/ocs/v2.php/apps/forms/", "/status.php"]; | |||
// checks if the accessed route is allowed for the user. | |||
// if it returns true, redirects elsewhere | |||
pub fn check_route(route: &str) -> bool { | |||
debug(route); | |||
for r in BLOCKED_ROUTES { | |||
if route.starts_with(r) { | |||
for s in ALLOWED_ROUTES { | |||
if route.starts_with(s) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
} | |||
false | |||
} |
@ -0,0 +1,61 @@ | |||
use actix_web::HttpRequest; | |||
use askama::Template; | |||
use crate::config::Config; | |||
#[derive(Template)] | |||
#[template(path = "index.html")] | |||
pub struct TplIndex<'a> { | |||
pub lang: &'a str, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "error.html")] | |||
pub struct TplError<'a> { | |||
pub lang: &'a str, | |||
pub error_msg: &'a str, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "link.html")] | |||
pub struct TplLink<'a> { | |||
pub lang: &'a str, | |||
pub config: &'a Config, | |||
} | |||
pub fn get_lang(req: &HttpRequest) -> String { | |||
// getting language from client header | |||
// taking the two first characters of the Accept-Language header, | |||
// in lowercase, then parsing it. | |||
// if it fails, returns "en" | |||
if let Some(la) = req.uri().query() { | |||
return la[5..].to_string(); | |||
} else { | |||
if let Some(l) = req.headers().get("Accept-Language") { | |||
if let Ok(s) = l.to_str() { | |||
return s.to_lowercase()[..2].to_string(); | |||
} | |||
} | |||
} | |||
String::from("en") | |||
} | |||
mod filters { | |||
use crate::config::LOC; | |||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||
let translation = LOC.get(key).ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the key {}", key); | |||
askama::Error::from(std::fmt::Error) | |||
})?; | |||
Ok(String::from( | |||
translation | |||
.get(lang) | |||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?) | |||
.as_str() | |||
.ok_or_else(|| { | |||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?, | |||
)) | |||
} | |||
} | |||
@ -0,0 +1 @@ | |||
Subproject commit b1fd3fccaeb98678c6a36973bac2666def4b3da1 |
@ -0,0 +1,65 @@ | |||
use actix_web::HttpRequest; | |||
use askama::Template; | |||
use crate::config::Config; | |||
#[derive(Template)] | |||
#[template(path = "index.html")] | |||
pub struct TplIndex<'a> { | |||
pub lang: &'a str, | |||
pub csrf_token: &'a str, | |||
pub sncf_admin_token: Option<String>, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "error.html")] | |||
pub struct TplError<'a> { | |||
pub lang: &'a str, | |||
pub error_msg: &'a str, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "link.html")] | |||
pub struct TplLink<'a> { | |||
pub lang: &'a str, | |||
pub admin_token: &'a str, | |||
pub csrf_token: &'a str, | |||
pub config: &'a Config, | |||
} | |||
pub fn get_lang(req: &HttpRequest) -> String { | |||
// getting language from client header | |||
// taking the two first characters of the Accept-Language header, | |||
// in lowercase, then parsing it. | |||
// if it fails, returns "en" | |||
if let Some(la) = req.uri().query() { | |||
return la[5..].to_string(); | |||
} else { | |||
if let Some(l) = req.headers().get("Accept-Language") { | |||
if let Ok(s) = l.to_str() { | |||
return s.to_lowercase()[..2].to_string(); | |||
} | |||
} | |||
} | |||
String::from("en") | |||
} | |||
mod filters { | |||
use crate::config::LOC; | |||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||
let translation = LOC.get(key).ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the key {}", key); | |||
askama::Error::from(std::fmt::Error) | |||
})?; | |||
Ok(String::from( | |||
translation | |||
.get(lang) | |||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?) | |||
.as_str() | |||
.ok_or_else(|| { | |||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?, | |||
)) | |||
} | |||
} | |||
@ -0,0 +1,65 @@ | |||
use actix_web::HttpRequest; | |||
use askama::Template; | |||
use crate::config::Config; | |||
#[derive(Template)] | |||
#[template(path = "index.html")] | |||
pub struct TplIndex<'a> { | |||
pub lang: &'a str, | |||
pub csrf_token: &'a str, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "error.html")] | |||
pub struct TplError<'a> { | |||
pub lang: &'a str, | |||
pub error_msg: &'a str, | |||
} | |||
#[derive(Template)] | |||
#[template(path = "link.html")] | |||
pub struct TplLink<'a> { | |||
pub lang: &'a str, | |||
pub admin_token: &'a str, | |||
pub config: &'a Config, | |||
} | |||
pub fn get_lang(req: &HttpRequest) -> String { | |||
// getting language from client header | |||
// taking the two first characters of the Accept-Language header, | |||
// in lowercase, then parsing it. | |||
// if it fails, returns "en" | |||
if let Some(l) = req.headers().get("Accept-Language") { | |||
if let Ok(s) = l.to_str() { | |||
return s.to_lowercase()[..2].to_string(); | |||
} | |||
} | |||
if let Some(l) = req.headers().get("lang") { | |||
if let Ok(s) = l.to_str() { | |||
return s.to_lowercase()[..2].to_string(); | |||
} | |||
} | |||
String::from("en") | |||
} | |||
mod filters { | |||
use crate::config::LOC; | |||
pub fn tr(key: &str, lang: &str) -> askama::Result<String> { | |||
let translation = LOC.get(key).ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the key {}", key); | |||
askama::Error::from(std::fmt::Error) | |||
})?; | |||
Ok(String::from( | |||
translation | |||
.get(lang) | |||
.unwrap_or(translation.get("en").ok_or_else(|| { | |||
eprintln!("tr filter: couldn't find the lang {} in key {}", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?) | |||
.as_str() | |||
.ok_or_else(|| { | |||
eprintln!("tr filter: lang {} in key {} is not str", lang, key); | |||
askama::Error::from(std::fmt::Error) | |||
})?, | |||
)) | |||
} | |||
} | |||
@ -1,46 +0,0 @@ | |||
FROM tensorflow/tensorflow:1.12.0-gpu | |||
COPY Prototyp /home/Prototyp | |||
COPY requis.txt /home/requis.txt | |||
RUN apt-get update && apt-get install -y wget libssl-dev openssl | |||
#RUN wget https://www.python.org/ftp/python/3.5.3/Python-3.5.3.tgz | |||
#RUN tar -xzvf Python-3.5.3.tgz | |||
#RUN cd Python-3.5.3 && ./configure && make && make install | |||
RUN python --version | |||
RUN apt-get update && apt-get install -y virtualenv python-dev python-pip build-essential | |||
#RUN python3.5 -m venv /home/venv | |||
#ENV PATH="home/venv/bin:$PATH" | |||
RUN python --version | |||
#RUN pip3 install --upgrade pip | |||
RUN pip install -r /home/requis.txt && python -m spacy download de | |||
RUN pip install hickle==3.4.9 Twisted joblib | |||
#nodejs npm | |||
#RUN python -m pip install incremental | |||
#RUN python -m pip install cffi | |||
#RUN python -m pip install -r /home/requis.txt | |||
#RUN python3 -m spacy download de | |||
#RUN pip3 install pandas bs4 | |||
RUN apt-get update && apt-get install -y nodejs | |||
#ENTRYPOINT ["tail"] | |||
#CMD ["-f","/dev/null"] | |||
CMD /bin/sh -c "cd /home/Prototyp && nodejs server.js" |
@ -0,0 +1,44 @@ | |||
FROM tensorflow/tensorflow:2.3.0-gpu | |||
# why 2.3 ? I looked it up on stack overflow | |||
# https://stackoverflow.com/questions/50622525/which-tensorflow-and-cuda-version-combinations-are-compatible | |||
# here is a nice list, which tf version is compatible with which cuda | |||
# from the cmmand docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi | |||
# you get your installed cuda version running | |||
RUN useradd -ms /bin/bash pluritonian | |||
COPY Translations.txt /home/pluritonian/Translations.txt | |||
COPY test_runwithgen.py /home/pluritonian/test_runwithgen.py | |||
COPY test_runwithload.py /home/pluritonian/test_runwithload.py | |||
COPY generateModels.py /home/pluritonian/generateModels.py | |||
COPY req.js /home/pluritonian/req.js | |||
COPY postcommand /home/pluritonian/postcommand | |||
COPY updateDatabase.py /home/pluritonian/updateDatabase.py | |||
COPY FASTsearch.py /home/pluritonian/FASTsearch.py | |||
COPY fastapi_server.py /home/pluritonian/fastapi_server.py | |||
#USER pluritonian | |||
WORKDIR /home/pluritonian | |||
RUN apt-get update && apt-get install nano | |||
RUN pip install joblib scikit-learn hickle==3.4.9 fastapi uvicorn[standard] | |||
RUN pip install idna==2.9 python-multipart==0.0.5 | |||
RUN python generateModels.py | |||
# to let the container running: | |||
CMD uvicorn --host 0.0.0.0 fastapi_server:app | |||
#ENTRYPOINT ["tail"] | |||
#CMD ["-f","/dev/null"] |
@ -0,0 +1,2 @@ | |||
[['Ich gehe nach Hause, weil es regnet.'], ['Ich gehe nach Hause. Weil es regnet.']] | |||
[['Es wäre sinnvoller, wenn die Maschinen aufhören zu regieren.'], ['Wenn die Maschinen aufhören zu regieren. Das ist sinnvoller.']] |
@ -0,0 +1,37 @@ | |||
from fastapi import FastAPI, Response, Request | |||
from fastapi.responses import JSONResponse | |||
app = FastAPI() | |||
from updateDatabase import * | |||
pluriDBupdater = PluritonUpdater() | |||
pluriDBupdater.loadModels() | |||
@app.post("/datext", response_class=JSONResponse) | |||
async def root(data: Request): | |||
text_bytes = await data.body() | |||
text = str(text_bytes) | |||
print(text) | |||
einfach, schwer = pluriDBupdater.searchNearest2Translate(text) | |||
einfachstr = '' | |||
schwerstr = '' | |||
for word in einfach: | |||
einfachstr += word + ' ' | |||
for word in schwer: | |||
schwerstr += word + ' ' | |||
daresponse = einfachstr + '?&?&' + schwerstr | |||
return JSONResponse(content=daresponse) | |||
@ -0,0 +1,18 @@ | |||
from updateDatabase import * | |||
print('Init Pluriton..') | |||
pluriDBupdater = PluritonUpdater() | |||
print('done') | |||
print('creaing hklDB from the Translations..') | |||
pluriDBupdater.create_hklDB_from_csv('Translations.txt') | |||
print('done') | |||
print('generating BOW models..') | |||
pluriDBupdater.load_DB_into_FASTsearch_and_generate_BOW() | |||
print('done') | |||
#pluriDBupdater.loadModels() | |||
#einfach, schwer = pluriDBupdater.searchNearest2Translate('Die Maschinen besser') | |||
#print('Schwer', schwer) | |||
@ -0,0 +1 @@ | |||
curl -X POST -H "Content-Type: application/json" -d @req.json http://localhost:8000/datext |
@ -0,0 +1,3 @@ | |||
{ | |||
"Text": "Die Maschinen werrden immer besser" | |||
} |
@ -0,0 +1,16 @@ | |||
from updateDatabase import * | |||
pluriDBupdater = PluritonUpdater() | |||
pluriDBupdater.create_hklDB_from_csv('Translations.txt') | |||
pluriDBupdater.load_DB_into_FASTsearch_and_generate_BOW() | |||
#pluriDBupdater.loadModels() | |||
einfach, schwer = pluriDBupdater.searchNearest2Translate('Die Maschinen besser') | |||
print('Schwer', schwer) | |||
@ -0,0 +1,11 @@ | |||
from updateDatabase import * | |||
pluriDBupdater = PluritonUpdater() | |||
pluriDBupdater.loadModels() | |||
einfach, schwer = pluriDBupdater.searchNearest2Translate('Die Maschinen besser') | |||
print('Schwer', schwer) | |||
@ -0,0 +1,126 @@ | |||
import hickle as hkl | |||
import FASTsearch | |||
class PluritonUpdater(object): | |||
def __init__(self): | |||
self.ole = 1 | |||
# Input: csv file with the form ['eine', 'schwere', 'Sprache'] , ['in', 'leicht'] for each line | |||
# Output: hkl dump of array in form [[['eine', 'schwere', 'Sprache'],['in', 'leicht']],[..]] | |||
def create_hklDB_from_csv(self, csvDbDir): | |||
with open(csvDbDir) as lines: | |||
TranslationsDB_All = [] | |||
for line in lines: | |||
TranslationsDB_All.append(list(eval(line))) | |||
#print(ShortsDB_All) | |||
#print(ShortsDB_All[0][0]) | |||
hkldbTranslations1 = [] | |||
hkldbTranslations2 = [] | |||
counter = 0 | |||
for n in range(len(TranslationsDB_All)): | |||
counter += 1 | |||
#if counter % 1000 == 0: | |||
#print(counter) | |||
hkldbTranslations1.append([TranslationsDB_All[n][0][0]]) | |||
hkldbTranslations2.append([TranslationsDB_All[n][1][0]]) | |||
#print(hkldbTranslations1, TranslationsDB_All) | |||
#print('creating the hkl dump of TranslationsDBAll') | |||
hkl.dump(TranslationsDB_All, 'hkldbTranslations_All.hkl', mode='w', compression='gzip') | |||
#print('done..') | |||
#print('Creating the hkl dump of TranslationsDB') | |||
hkl.dump(hkldbTranslations1, 'hkldbTranslations1.hkl', mode='w', compression='gzip') | |||
hkl.dump(hkldbTranslations2, 'hkldbTranslations2.hkl', mode='w', compression='gzip') | |||
#print('done..') | |||
return 'done' | |||
def load_DB_into_FASTsearch_and_generate_BOW(self): | |||
print('loading the hkldbTranslations1...') | |||
self.hkldbTranslations1 = hkl.load('hkldbTranslations1.hkl') | |||
print('done') | |||
print('loading the hkldbTranslations2...') | |||
self.hkldbTranslations2 = hkl.load('hkldbTranslations2.hkl') | |||
print('done') | |||
print('loading hkldbTranslations 1 into FASTsearch..') | |||
self.fsearch1 = FASTsearch.FASTsearch('hkldbTranslations1.hkl') | |||
print('done') | |||
print('loading hkldbTranslations 2 into FASTsearch..') | |||
self.fsearch2 = FASTsearch.FASTsearch('hkldbTranslations2.hkl') | |||
print('done') | |||
print('generating BoW Model 1..') | |||
self.fsearch1.Gen_BoW_Model(50000, "word", punctuation = False) | |||
print('done') | |||
print('generating BoW Model 2..') | |||
self.fsearch2.Gen_BoW_Model(50000, "word", punctuation = False) | |||
print('done') | |||
return 'done' | |||
def loadModels(self): | |||
print('loading the hkldbTranslations1...') | |||
self.hkldbTranslations1 = hkl.load('hkldbTranslations1.hkl') | |||
print('done') | |||
print('loading the hkldbTranslations2...') | |||
self.hkldbTranslations2 = hkl.load('hkldbTranslations2.hkl') | |||
print('done') | |||
print('loading hkldbTranslations 1 into FASTsearch..') | |||
self.fsearch1 = FASTsearch.FASTsearch('hkldbTranslations1.hkl') | |||
print('done') | |||
print('loading hkldbTranslations 2 into FASTsearch..') | |||
self.fsearch2 = FASTsearch.FASTsearch('hkldbTranslations2.hkl') | |||
print('done') | |||
print('loading the bow model 1') | |||
self.fsearch1.Load_BoW_Model('bagofwordshkldbTranslations1.pkl', 'DataBaseOneZeroshkldbTranslations1.hkl') | |||
print('done') | |||
print('loading the bow model 2') | |||
self.fsearch2.Load_BoW_Model('bagofwordshkldbTranslations2.pkl', 'DataBaseOneZeroshkldbTranslations2.hkl') | |||
print('done') | |||
return 'done' | |||
def searchNearest2Translate(self, text): | |||
bestmatches2, matchindex2 = self.fsearch1.search_with_highest_multiplikation_Output(text, 1) | |||
DifficultText = self.hkldbTranslations1[matchindex2[0]][0].split() | |||
LeichterText = self.hkldbTranslations2[matchindex2[0]][0].split() | |||
return DifficultText, LeichterText | |||
@ -1,12 +1,33 @@ | |||
version: '2.3' | |||
version: '3.1' | |||
services: | |||
prototype: | |||
pluriton: | |||
build: ../build/tf-gpu-Prototyp | |||
container_name: prototype | |||
build: ../build/tfgpu-pluriton | |||
container_name: pluriton_python_app | |||
restart: always | |||
deploy: | |||
resources: | |||
reservations: | |||
devices: | |||
- capabilities: [gpu] | |||
networks: | |||
- pluritonNet | |||
deb-rust-pluriton-interface: | |||
build: ../build/deb-rust-pluriton-interface | |||
container_name: deb-rust-pluriton-interface | |||
restart: always | |||
environment: | |||
- RUST_BACKTRACE=full | |||
ports: | |||
- "127.0.0.1:7000:7000" | |||
- "127.0.0.1:1020:7050" | |||
networks: | |||
- pluritonNet | |||
networks: | |||
pluritonNet: | |||
driver: bridge |