docker+rust

This commit is contained in:
alpcentaur 2021-10-18 18:22:03 +02:00
parent be4279eb64
commit 27f51a7114
47 changed files with 5250 additions and 52 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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

View file

@ -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

View file

@ -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]
}

File diff suppressed because one or more lines are too long

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 ©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;
}

View file

@ -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>

View file

@ -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>

After

Width:  |  Height:  |  Size: 211 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 919 B

View file

@ -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(&params.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(&params.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")
})?)
}

View file

@ -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(&params.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(&params.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")
})?)
}

View file

@ -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(&params.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(&params.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")
})?)
}

View file

@ -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(&params.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(&params.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")
})?)
}

View file

@ -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);
}
}

View file

@ -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&ccedil;ais</option>
<option lang="en" selected value="en">English</option>
<option lang="oc" value="oc">Occitan</option>
<option lang="es" value="es">Espa&ntilde;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&colon;&sol;&sol;foorms&period;digitalcourage&period;de&sol;" 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>

View file

@ -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."
}
}

View file

@ -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&ccedil;ais</option>
<option lang="en" selected value="en">English</option>
<option lang="oc" value="oc">Occitan</option>
<option lang="es" value="es">Espa&ntilde;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&colon;&sol;&sol;foorms&period;digitalcourage&period;de&sol;" 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>

View file

@ -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);
}
}

View file

@ -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!();
}
}
}

View file

@ -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,
}
}
}

View file

@ -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")
})?)
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
})?,
))
}
}

View file

@ -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)
})?,
))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -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"

View file

@ -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"]

View file

@ -13,8 +13,9 @@ from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
import scipy as sc
import tensorflow as tf
import tensorflow.compat.v1 as tf
tf.compat.v1.disable_eager_execution()
import _pickle as cPickle

View file

@ -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.']]

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1 @@
curl -X POST -H "Content-Type: application/json" -d @req.json http://localhost:8000/datext

View file

@ -0,0 +1,3 @@
{
"Text": "Die Maschinen werrden immer besser"
}

View file

@ -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)

View file

@ -0,0 +1,11 @@
from updateDatabase import *
pluriDBupdater = PluritonUpdater()
pluriDBupdater.loadModels()
einfach, schwer = pluriDBupdater.searchNearest2Translate('Die Maschinen besser')
print('Schwer', schwer)

View file

@ -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

View file

@ -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

0
in Normal file
View file

0
out.html Normal file
View file