diff --git a/.gitignore b/.gitignore index 62bd1a4..7708962 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk +/config.toml +/db/sncf.sqlite diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bad3f4d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sncf" +version = "1.0.0" +authors = ["Association 42l "] +edition = "2018" + +[dependencies] +actix-rt = "1.0.0" +actix-web = "3.0.0-beta.3" +actix-files = "0.3.0-beta.1" +diesel = { version = "1.4", features = ["sqlite", "r2d2", "chrono"] } +diesel_migrations = "1.4" +url = "2.0" +toml = "0.5" +lazy_static = "1.4" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" +chrono = { version = "0.4", features = ["serde"] } +regex = "1.3" +base64 = "0.12" +rand = "0.7" +askama = "0.10" diff --git a/adj-list.txt b/adj-list.txt new file mode 100644 index 0000000..0fff840 --- /dev/null +++ b/adj-list.txt @@ -0,0 +1,141 @@ +Adorable +Adventurous +Aggressive +Agreeable +Ambitious +Amused +Annoying +Ashy +Attractive +Beautiful +Better +Black +Blue +Blushing +Brave +Bright +Busy +Calm +Careful +Cautious +Charming +Cheerful +Clever +Clumsy +Colorful +Combative +Confident +Cooperative +Courageous +Crazy +Creative +Creepy +Cruel +Curious +Cute +Dangerous +Dark +Dazzling +Delightful +Determined +Different +Distinct +Dizzy +Eager +Elegant +Embarrassed +Enchanting +Encouraging +Energetic +Enthusiastic +Evil +Excited +Fair +Faithful +Famous +Fancy +Fantastic +Fierce +Fine +Friendly +Funny +Gentle +Gifted +Glad +Glamorous +Gleaming +Glorious +Good +Gorgeous +Graceful +Gray +Green +Grumpy +Handsome +Happy +Hardy +Healthy +Helpful +Hilarious +Homely +Hungry +Icy +Important +Innocent +Inquisitive +Jolly +Joyful +Joyous +Kind +Lazy +Light +Lively +Lovely +Loving +Lucky +Magnificent +Marvelous +Misty +Modern +Muscular +Mysterious +Nice +Obedient +Optimistic +Orange +Outstanding +Perfect +Pleasant +Polite +Powerful +Precious +Proud +Purple +Red +Scary +Shiny +Shy +Silly +Skinny +Sleepy +Smiling +Sparkling +Splendid +Spotless +Stormy +Strange +Super +Talented +Tenacious +Thankful +Thoughtful +Tired +Tough +Unusual +Victorious +Vivacious +White +Wild +Witty +Wonderful +Yellow diff --git a/config.toml.sample b/config.toml.sample new file mode 100644 index 0000000..5546acd --- /dev/null +++ b/config.toml.sample @@ -0,0 +1,26 @@ +# 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://localhost:8000" + +# path to the SQLite DB +database_path = "./db/sncf.sqlite" + +# IP address of the Nextcloud instance, including protocol and port +nextcloud_url = "http://10.0.0.0" + +# Nextcloud admin account +admin_username = "adminusername" +admin_password = "adminverylongandsecurepassword" + +# How many days of inactivity for an admin token before deleting NC accounts +prune_days = 150 + +# Displays route names and a lot of information +debug_mode = true + +# Don't touch this unless you know what you're doing +config_version = 1 diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/db.sqlite b/db/db.sqlite new file mode 100644 index 0000000..e69de29 diff --git a/lang.json b/lang.json new file mode 100644 index 0000000..9abe1e7 --- /dev/null +++ b/lang.json @@ -0,0 +1,322 @@ +{ + "lang_code": { + "en": "en", + "fr": "fr" + }, + "lang_full": { + "en": "English", + "fr": "Français" + }, + "meta_description": { + "en": "42l Forms : create forms for free, without registration while protecting your privacy", + "fr": "42l Formulaires (Forms) : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée" + }, + "index_title": { + "en": "42l Forms", + "fr": "42l Formulaires" + }, + "index_description": { + "en": "Create forms without registration", + "fr": "Créez des questionnaires sans inscription" + }, + "index_beta_tag": { + "en": "BETA", + "fr": "BETA" + }, + "index_createform_button": { + "en": "Create a form", + "fr": "Créer un formulaire" + }, + "index_beta_banner_title": { + "en": "Warning: Service in beta.", + "fr": "Attention : Service en bêta." + }, + "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." + }, + "index_beta_banner_desc2": { + "en": "Feel free to send feedbacks on our ", + "fr": "Vous pouvez nous envoyer vos retours sur " + }, + "index_beta_banner_desc_link": { + "en": "our contact page", + "fr": "notre page de contact" + }, + "index_disclaimer1": { + "en": "This service is maintained for free, without subscription nor advertising nor tracking or selling of your personal data, on a server hosted in France.", + "fr": "Ce service vous est fourni gratuitement, sans inscription, sans publicités, sans pistage ou revente de vos données personnelles, sur un serveur hébergé en France." + }, + "index_disclaimer2": { + "en": "If you appreciate our work, please consider donating to ", + "fr": "Si vous appréciez notre travail, merci d'envisager de faire un don à " + }, + "index_disclaimer2_link_org": { + "en": "the 42l association", + "fr": "l'association 42l" + }, + "index_disclaimer2_or": { + "en": " or ", + "fr": " ou à " + }, + "index_disclaimer2_nc": { + "en": "Nextcloud", + "fr": "Nextcloud" + }, + "index_panel1_title": { + "en": "Responsive and intuitive interface", + "fr": "Interface intuitive et compatible mobile" + }, + "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 ?" + }, + "index_panel1_desc2": { + "en": "You've just found it.", + "fr": "Vous venez de la trouver." + }, + "index_panel2_title": { + "en": "Choose and order your fields", + "fr": "Choisissez et ordonnez vos champs" + }, + "index_panel2_desc1": { + "en": "The software currently supports five field types.", + "fr": "Pour le moment, le logiciel supporte cinq types de champs." + }, + "index_panel2_desc2": { + "en": "New field types are ", + "fr": "De nouveaux types de champs sont " + }, + "index_panel2_desc2_link": { + "en": "currently in the works", + "fr": "en cours d'élaboration" + }, + "index_panel3_title": { + "en": "Analyze the answers", + "fr": "Analysez les réponses" + }, + "index_panel3_desc1": { + "en": "See detailed graphs of the answers to your form.", + "fr": "Visualisez les réponses à votre formulaire avec un graphique." + }, + "index_panel4_title": { + "en": "Export the answers", + "fr": "Exportez les réponses" + }, + "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)." + }, + "index_panel5_title": { + "en": "Edit your form's settings", + "fr": "Paramétrez vos formulaires" + }, + "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." + }, + "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." + }, + "index_panel6_title": { + "en": "All your forms in one place", + "fr": "Tous vos formulaires au même endroit" + }, + "index_panel6_desc1": { + "en": "Find all your forms in the same panel.", + "fr": "Retrouvez tous vos formulaires sur un même panel." + }, + "index_bottom_docs": { + "en": "Documentation", + "fr": "Documentation" + }, + "index_bottom_source": { + "en": "Source code", + "fr": "Code source" + }, + "index_bottom_lic": { + "en": "License", + "fr": "Licence" + }, + "index_credits_title": { + "en": "Credits", + "fr": "Crédits" + }, + "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 " + }, + "index_credits_desc1_link": { + "en": "the Nextcloud team", + "fr": "l'équipe Nextcloud" + }, + "index_credits_desc1_a": { + "en": " and its contributors.", + "fr": " et ses contributeur·ices." + }, + "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 " + }, + "index_credits_desc2_for": { + "en": " for ", + "fr": " pour " + }, + "index_credits_desc2_org": { + "en": "the 42l association", + "fr": "l'association 42l" + }, + "index_credits_desc3": { + "en": "(sources available soon)", + "fr": "(sources bientôt disponibles)" + }, + "link_title": { + "en": "Link created", + "fr": "Lien créé" + }, + "link_desc1": { + "en": "Here's an administration link, which will allow you to access all your forms and check your answers.", + "fr": "Voici un lien d'administration, qui vous permettra d'accéder à tous vos formulaires et de consulter vos réponses." + }, + "link_desc2": { + "en": "Keep it carefully and don't give it away (it'd be the same as giving out your password!).", + "fr": "Conservez-le bien précieusement et ne le donnez pas (cela reviendrait à donner un mot de passe !)." + }, + "link_desc3": { + "en": "Once your link copied, click on the button below to start editing your forms.", + "fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour commencer à éditer vos formulaires." + }, + "link_access_btn": { + "en": "Access the forms", + "fr": "Accéder aux formulaires" + }, + "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 " + }, + "link_note2": { + "en": " days, your forms will be automatically deleted.", + "fr": " jours, vos formulaires seront automatiquement supprimés." + }, + "link_copy": { + "en": "Copy link", + "fr": "Copier le lien" + }, + "link_copied": { + "en": "Link copied!", + "fr": "Lien copié !" + }, + "error_title": { + "en": "Oops!...", + "fr": "Oups !..." + }, + "error_description": { + "en": "The application encountered a problem:", + "fr": "L'application a rencontré un problème :" + }, + "error_back": { + "en": "Back to the main page", + "fr": "Retour à la page principale" + }, + "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." + }, + "error_note2": { + "en": "Sorry for the inconvenience.", + "fr": "Désolés pour les désagréments occasionnés." + }, + "error_forward_req": { + "en": "Error while connecting to the Nextcloud instance.", + "fr": "Erreur lors de la connexion à l'instance Nextcloud." + }, + "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." + }, + "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." + }, + "error_forwardlogin_db": { + "en": "Couldn't connect to the local database.", + "fr": "Échec lors de la connexion à la base de données locale." + }, + "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." + }, + "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é." + }, + "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é." + }, + "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é." + }, + "error_login_redir": { + "en": "Redirection to Nextcloud accout failed.", + "fr": "La redirection vers le compte Nextcloud a échoué." + }, + "error_createaccount_post": { + "en": "Account creation: connection to the Nextcloud API failed.", + "fr": "Création de compte : la connexion à l'API Nextcloud a échoué." + }, + "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é." + }, + "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." + }, + "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." + }, + "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." + }, + "error_forwardregister_pool": { + "en": "Error while connecting to the local database.", + "fr": "Erreur lors de la connexion à la base de données locale." + }, + "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é." + }, + "error_login_cookiepair": { + "en": "Couldn't read cookies.", + "fr": "Échec lors de la lecture de cookies." + }, + "error_login_regex": { + "en": "Couldn't read the CSRF token.", + "fr": "Échec lors de la lecture du token CSRF." + }, + "error_login_setcookie": { + "en": "Error during cookies transfer.", + "fr": "Erreur lors du transfert de cookies." + }, + "error_form_insert": { + "en": "The local database couldn't be reached", + "fr": "Échec de la connexion avec la base de données locale" + }, + "error_createaccount": { + "en": "The Nextcloud API returned an unexpected result", + "fr": "L'API de Nextcloud a retourné un résultat inattendu" + }, + "error_dirtyhacker": { + "en": "Attempt to access an unauthorized resource.", + "fr": "Tentative d'accès à une ressource non autorisée." + }, + "error_tplrender": { + "en": "Template rendering failed.", + "fr": "Le rendu du template a échoué." + } +} diff --git a/migrations/20200809180000_create_form/down.sql b/migrations/20200809180000_create_form/down.sql new file mode 100644 index 0000000..5605fc4 --- /dev/null +++ b/migrations/20200809180000_create_form/down.sql @@ -0,0 +1 @@ +DELETE TABLE form; diff --git a/migrations/20200809180000_create_form/up.sql b/migrations/20200809180000_create_form/up.sql new file mode 100644 index 0000000..1302b0f --- /dev/null +++ b/migrations/20200809180000_create_form/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE form ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL, + lastvisit_at TIMESTAMP NOT NULL, + token VARCHAR UNIQUE NOT NULL, + nc_username VARCHAR UNIQUE NOT NULL, + nc_password VARCHAR NOT NULL +); diff --git a/name-list.txt b/name-list.txt new file mode 100644 index 0000000..e4c317f --- /dev/null +++ b/name-list.txt @@ -0,0 +1,880 @@ +Abomasnow +Abra +Absol +Accelgor +Aegislash +Aerodactyl +Aggron +Aipom +Alakazam +Alcremie +Alomomola +Altaria +Amaura +Ambipom +Amoonguss +Ampharos +Anorith +Appletun +Applin +Araquanid +Arbok +Arcanine +Arceus +Archen +Archeops +Arctovish +Arctozolt +Ariados +Armaldo +Aromatisse +Aron +Arrokuda +Articuno +Audino +Aurorus +Avalugg +Axew +Azelf +Azumarill +Azurill +Bagon +Baltoy +Banette +Barbaracle +Barboach +Barraskewda +Basculin +Bastiodon +Bayleef +Beartic +Beautifly +Beedrill +Beheeyem +Beldum +Bellossom +Bellsprout +Bergmite +Bewear +Bibarel +Bidoof +Binacle +Bisharp +Blacephalon +Blastoise +Blaziken +Blipbug +Blissey +Blitzle +Boldore +Boltund +Bonsly +Bouffalant +Bounsweet +Braixen +Braviary +Breloom +Brionne +Bronzong +Bronzor +Bruxish +Budew +Buizel +Bulbasaur +Buneary +Bunnelby +Burmy +Butterfree +Buzzwole +Cacnea +Cacturne +Calyrex +Camerupt +Carbink +Carkol +Carnivine +Carracosta +Carvanha +Cascoon +Castform +Caterpie +Celebi +Celesteela +Centiskorch +Chandelure +Chansey +Charizard +Charjabug +Charmander +Charmeleon +Chatot +Cherrim +Cherubi +Chesnaught +Chespin +Chewtle +Chikorita +Chimchar +Chimecho +Chinchou +Chingling +Cinccino +Cinderace +Clamperl +Clauncher +Clawitzer +Claydol +Clefable +Clefairy +Cleffa +Clobbopus +Cloyster +Coalossal +Cobalion +Cofagrigus +Combee +Combusken +Comfey +Conkeldurr +Copperajah +Corphish +Corsola +Corviknight +Corvisquire +Cosmoem +Cosmog +Cottonee +Crabominable +Crabrawler +Cradily +Cramorant +Cranidos +Crawdaunt +Cresselia +Croagunk +Crobat +Croconaw +Crustle +Cryogonal +Cubchoo +Cubone +Cufant +Cursola +Cutiefly +Cyndaquil +Darkrai +Darmanitan +Dartrix +Darumaka +Decidueye +Dedenne +Deerling +Deino +Delcatty +Delibird +Delphox +Deoxys +Dewgong +Dewott +Dewpider +Dhelmise +Dialga +Diancie +Diggersby +Diglett +Ditto +Dodrio +Doduo +Donphan +Dottler +Doublade +Dracovish +Dracozolt +Dragalge +Dragapult +Dragonair +Dragonite +Drakloak +Drampa +Drapion +Dratini +Drednaw +Dreepy +Drifblim +Drifloon +Drilbur +Drizzile +Drowzee +Druddigon +Dubwool +Ducklett +Dugtrio +Dunsparce +Duosion +Duraludon +Durant +Dusclops +Dusknoir +Duskull +Dustox +Dwebble +Eelektrik +Eelektross +Eevee +Eiscue +Ekans +Eldegoss +Electabuzz +Electivire +Electrike +Electrode +Elekid +Elgyem +Emboar +Emolga +Empoleon +Entei +Escavalier +Espeon +Espurr +Eternatus +Excadrill +Exeggcute +Exeggutor +Exploud +Falinks +Farfetch’d +Fearow +Feebas +Fennekin +Feraligatr +Ferroseed +Ferrothorn +Finneon +Flaaffy +Flabebe +Flapple +Flareon +Fletchinder +Fletchling +Floatzel +Floette +Florges +Flygon +Fomantis +Foongus +Forretress +Fraxure +Frillish +Froakie +Frogadier +Froslass +Frosmoth +Furfrou +Furret +Gabite +Gallade +Galvantula +Garbodor +Garchomp +Gardevoir +Gastly +Gastrodon +Genesect +Gengar +Geodude +Gible +Gigalith +Girafarig +Giratina +Glaceon +Glalie +Glameow +Gligar +Gliscor +Gloom +Gogoat +Golbat +Goldeen +Golduck +Golem +Golett +Golisopod +Golurk +Goodra +Goomy +Gorebyss +Gossifleur +Gothita +Gothitelle +Gothorita +Gourgeist +Granbull +Grapploct +Graveler +Greedent +Greninja +Grimer +Grimmsnarl +Grookey +Grotle +Groudon +Grovyle +Growlithe +Grubbin +Grumpig +Gulpin +Gumshoos +Gurdurr +Guzzlord +Gyarados +Happiny +Hariyama +Hatenna +Hatterene +Hattrem +Haunter +Hawlucha +Haxorus +Heatmor +Heatran +Heliolisk +Helioptile +Heracross +Herdier +Hippopotas +Hippowdon +Hitmonchan +Hitmonlee +Hitmontop +Honchkrow +Honedge +Hoopa +Hoothoot +Hoppip +Horsea +Houndoom +Houndour +Huntail +Hydreigon +Hypno +Igglybuff +Illumise +Impidimp +Incineroar +Indeedee +Infernape +Inkay +Inteleon +Ivysaur +Jellicent +Jigglypuff +Jirachi +Jolteon +Joltik +Jumpluff +Jynx +Kabuto +Kabutops +Kadabra +Kakuna +Kangaskhan +Karrablast +Kartana +Kecleon +Keldeo +Kingdra +Kingler +Kirlia +Klang +Klefki +Klink +Klinklang +Koffing +Komala +Krabby +Kricketot +Kricketune +Krokorok +Krookodile +Kubfu +Kyogre +Kyurem +Lairon +Lampent +Landorus +Lanturn +Lapras +Larvesta +Larvitar +Latias +Latios +Leafeon +Leavanny +Ledian +Ledyba +Lickilicky +Lickitung +Liepard +Lileep +Lilligant +Lillipup +Linoone +Litleo +Litten +Litwick +Lombre +Lopunny +Lotad +Loudred +Lucario +Ludicolo +Lugia +Lumineon +Lunala +Lunatone +Lurantis +Luvdisc +Luxio +Luxray +Lycanroc +Machamp +Machoke +Machop +Magby +Magcargo +Magearna +Magikarp +Magmar +Magmortar +Magnemite +Magneton +Magnezone +Makuhita +Malamar +Mamoswine +Manaphy +Mandibuzz +Manectric +Mankey +Mantine +Mantyke +Maractus +Mareanie +Mareep +Marill +Marowak +Marshadow +Marshtomp +Masquerain +Mawile +Medicham +Meditite +Meganium +Melmetal +Meloetta +Meltan +Meowstic +Meowth +Mesprit +Metagross +Metang +Metapod +Mew +Mewtwo +Mienfoo +Mienshao +Mightyena +Milcery +Milotic +Miltank +Mimikyu +Minccino +Minior +Minun +Misdreavus +Mismagius +Moltres +Monferno +Morelull +Morgrem +Morpeko +Mothim +Mudbray +Mudkip +Mudsdale +Muk +Munchlax +Munna +Murkrow +Musharna +Naganadel +Natu +Necrozma +Nickit +Nidoking +Nidoqueen +Nidoran +Nidorina +Nidorino +Nihilego +Nincada +Ninetales +Ninjask +Noctowl +Noibat +Noivern +Nosepass +Numel +Nuzleaf +Obstagoon +Octillery +Oddish +Omanyte +Omastar +Onix +Oranguru +Orbeetle +Oricorio +Oshawott +Pachirisu +Palkia +Palossand +Palpitoad +Pancham +Pangoro +Panpour +Pansage +Pansear +Paras +Parasect +Passimian +Patrat +Pawniard +Pelipper +Perrserker +Persian +Petilil +Phanpy +Phantump +Pheromosa +Phione +Pichu +Pidgeot +Pidgeotto +Pidgey +Pidove +Pignite +Pikachu +Pikipek +Piloswine +Pincurchin +Pineco +Pinsir +Piplup +Plusle +Poipole +Politoed +Poliwag +Poliwhirl +Poliwrath +Polteageist +Ponyta +Poochyena +Popplio +Porygon +Primarina +Primeape +Prinplup +Probopass +Psyduck +Pumpkaboo +Pupitar +Purrloin +Purugly +Pyroar +Pyukumuku +Quagsire +Quilava +Quilladin +Qwilfish +Raboot +Raichu +Raikou +Ralts +Rampardos +Rapidash +Raticate +Rattata +Rayquaza +Regice +Regidrago +Regieleki +Regigigas +Regirock +Registeel +Relicanth +Remoraid +Reshiram +Reuniclus +Rhydon +Rhyhorn +Rhyperior +Ribombee +Rillaboom +Riolu +Rockruff +Roggenrola +Rolycoly +Rookidee +Roselia +Roserade +Rotom +Rowlet +Rufflet +Runerigus +Sableye +Salamence +Salandit +Salazzle +Samurott +Sandaconda +Sandile +Sandshrew +Sandslash +Sandygast +Sawk +Sawsbuck +Scatterbug +Sceptile +Scizor +Scolipede +Scorbunny +Scrafty +Scraggy +Scyther +Seadra +Seaking +Sealeo +Seedot +Seel +Seismitoad +Sentret +Serperior +Servine +Seviper +Sewaddle +Sharpedo +Shaymin +Shedinja +Shelgon +Shellder +Shellos +Shelmet +Shieldon +Shiftry +Shiinotic +Shinx +Shroomish +Shuckle +Shuppet +Sigilyph +Silcoon +Silicobra +Silvally +Simipour +Simisage +Simisear +Sinistea +Sizzlipede +Skarmory +Skiddo +Skiploom +Skitty +Skorupi +Skrelp +Skuntank +Skwovet +Slaking +Slakoth +Sliggoo +Slowbro +Slowking +Slowpoke +Slugma +Slurpuff +Smeargle +Smoochum +Sneasel +Snivy +Snom +Snorlax +Snorunt +Snover +Snubbull +Sobble +Solgaleo +Solosis +Solrock +Spearow +Spewpa +Spheal +Spinarak +Spinda +Spiritomb +Spoink +Spritzee +Squirtle +Stakataka +Stantler +Staraptor +Staravia +Starly +Starmie +Staryu +Steelix +Steenee +Stonjourner +Stoutland +Stufful +Stunfisk +Stunky +Sudowoodo +Suicune +Sunflora +Sunkern +Surskit +Swablu +Swadloon +Swalot +Swampert +Swanna +Swellow +Swinub +Swirlix +Swoobat +Sylveon +Taillow +Talonflame +Tangela +Tangrowth +Tauros +Teddiursa +Tentacool +Tentacruel +Tepig +Terrakion +Thievul +Throh +Thundurus +Thwackey +Timburr +Tirtouga +Togedemaru +Togekiss +Togepi +Togetic +Torchic +Torkoal +Tornadus +Torracat +Torterra +Totodile +Toucannon +Toxapex +Toxel +Toxicroak +Toxtricity +Tranquill +Trapinch +Treecko +Trevenant +Tropius +Trubbish +Trumbeak +Tsareena +Turtonator +Turtwig +Tympole +Tynamo +Typhlosion +Tyranitar +Tyrantrum +Tyrogue +Tyrunt +Umbreon +Unfezant +Unown +Ursaring +Urshifu +Uxie +Vanillish +Vanillite +Vanilluxe +Vaporeon +Venipede +Venomoth +Venonat +Venusaur +Vespiquen +Vibrava +Victini +Victreebel +Vigoroth +Vikavolt +Vileplume +Virizion +Vivillon +Volbeat +Volcanion +Volcarona +Voltorb +Vullaby +Vulpix +Wailmer +Wailord +Walrein +Wartortle +Watchog +Weavile +Weedle +Weepinbell +Weezing +Whimsicott +Whirlipede +Whiscash +Whismur +Wigglytuff +Wimpod +Wingull +Wishiwashi +Wobbuffet +Woobat +Wooloo +Wooper +Wormadam +Wurmple +Wynaut +Xatu +Xerneas +Xurkitree +Yamask +Yampur +Yanma +Yanmega +Yungoos +Yveltal +Zacian +Zamazenta +Zangoose +Zapdos +Zarude +Zebstrika +Zekrom +Zeraora +Zigzagoon +Zoroark +Zorua +Zubat +Zweilous +Zygarde diff --git a/src/account.rs b/src/account.rs new file mode 100644 index 0000000..1a6dcd7 --- /dev/null +++ b/src/account.rs @@ -0,0 +1,243 @@ +use actix_web::client::Client; +use actix_web::{http, web, HttpRequest, HttpResponse}; +use base64::URL_SAFE_NO_PAD; +use rand::rngs::OsRng; +use rand::Rng; +use rand::RngCore; +use regex::Regex; + +use crate::templates::get_lang; +use crate::config::{ADJ_LIST, NAME_LIST}; +use crate::errors::{crash, TrainCrash}; +use crate::debug; +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 + } +} + +pub fn has_admintoken(req: &HttpRequest) -> Option<&str> { + let c = req.headers().get("Cookie")?.to_str().ok()?; + if c.contains("sncf_admin_token") { + 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, + user: &str, + password: &str, + lang: String, +) -> Result { + let mut register_query = client + .post(format!( + "{}/{}", + CONFIG.nextcloud_url, "ocs/v1.php/cloud/users" + )) + .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()); + 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("").ok_or_else(|| { + eprintln!("error_createaccount_ncstatus_parse: start missing"); + crash(lang.clone(), "error_createaccount_ncstatus_parse") + })? + 12; + let status_end = response_body.find("").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::() { + 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, + req: &HttpRequest, + user: &str, + password: &str, +) -> Result { + debug(&format!("Sending forged login for user {}", user)); + + // 1. GET /login + let mut login_get = client + .get(format!("{}/{}", CONFIG.nextcloud_url, "login")) + .header("User-Agent", "Actix-web") + .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(); + for h_value in login_get.headers().get_all("set-cookie") { + str_cookiepair = format!("{}; {}", str_cookiepair, h_value.clone().to_str().map_err(|e| { + eprintln!("error_login_cookiepair: {}", e); + crash(get_lang(&req), "error_login_cookiepair") + })?); + } + + // load requesttoken regex + lazy_static! { + static ref RE: Regex = Regex::new(r#"requesttoken="(?P.*)""#).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")) + .header("User-Agent", "Actix-web"); + + // 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(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() -> String { + // Using /dev/random to generate random bytes + let mut r = OsRng; + + let mut my_secure_bytes = vec![0u8; 45]; + r.fill_bytes(&mut my_secure_bytes); + base64::encode_config(my_secure_bytes, URL_SAFE_NO_PAD) +} + +pub fn gen_name() -> String { + format!("{}{}", list_rand(&ADJ_LIST), list_rand(&NAME_LIST)) +} + +pub fn list_rand(list: &Vec) -> &String { + let mut rng = rand::thread_rng(); + let roll = rng.gen_range(0, list.len() - 1); + &list[roll] +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c1878eb --- /dev/null +++ b/src/config.rs @@ -0,0 +1,71 @@ +use std::fs::File; +use std::io::Read; +use std::io::{self, BufRead, BufReader}; +use std::path::Path; +use serde_json::Value; + +// payload limit set to 5MiB +pub const PAYLOAD_LIMIT: usize = 50_000_000; + +pub const CONFIG_FILE: &str = "./config.toml"; +pub const CONFIG_VERSION: u8 = 1; + +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"; + +lazy_static! { + pub static ref CONFIG: Config = Config::init(); + pub static ref ADJ_LIST: Vec = + lines_from_file(ADJ_LIST_FILE).expect("Failed to load adjectives list"); + pub static ref NAME_LIST: Vec = + lines_from_file(NAME_LIST_FILE).expect("Failed to load names list"); + pub static ref LOC: Value = init_lang(); +} + +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") +} + +fn lines_from_file(filename: impl AsRef) -> io::Result> { + BufReader::new(File::open(filename)?).lines().collect() +} + +#[derive(Deserialize)] +pub struct Config { + pub listening_address: String, + pub listening_port: u16, + pub sncf_url: String, + pub database_path: String, + pub nextcloud_url: String, + pub admin_username: String, + pub admin_password: String, + pub prune_days: u16, + pub debug_mode: bool, + pub config_version: u8, +} + +// totally not copypasted from rs-short +impl Config { + 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") + } + pub fn check_version(&self) { + if self.config_version != CONFIG_VERSION { + eprintln!("Your configuration file is obsolete! Please update it with config.toml.sample and update its version to {}.", CONFIG_VERSION); + panic!(); + } + } +} diff --git a/src/database/methods.rs b/src/database/methods.rs new file mode 100644 index 0000000..14f0178 --- /dev/null +++ b/src/database/methods.rs @@ -0,0 +1,50 @@ +use chrono::NaiveDateTime; +use chrono::Utc; +use diesel::prelude::*; + +use crate::database::schema::form::dsl::*; +use crate::database::structs::Form; +use crate::database::schema::form; +use crate::SqliteConnection; + +#[table_name = "form"] +#[derive(Serialize, Insertable)] +pub struct InsertableForm<'b> { + pub created_at: NaiveDateTime, + pub lastvisit_at: NaiveDateTime, + pub token: &'b str, + pub nc_username: &'b str, + pub nc_password: &'b str, +} + +impl Form { + // gets a Form from a token. + // also updates lastvisit_at. + pub fn get_from_token( + i_token: &str, + conn: &SqliteConnection, + ) -> Result, diesel::result::Error> { + if let Some(formdata) = form.filter(token.eq(i_token)).first::
(conn).optional()? { + match formdata.update_lastvisit(conn) { + Ok(_) => Ok(Some(formdata)), + Err(e) => Err(e), + } + } else { + Ok(None) + } + } + + pub fn update_lastvisit(&self, conn: &SqliteConnection) -> Result { + diesel::update(form.find(self.id)) + .set(lastvisit_at.eq(Utc::now().naive_utc())) + .execute(conn) + } + + pub fn insert<'b>(i_form: InsertableForm<'b>, conn: &SqliteConnection + ) -> Result, diesel::result::Error> { + match diesel::insert_into(form).values(&i_form).execute(conn) { + Ok(_) => Ok(i_form), + Err(e) => Err(e), + } + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..252677d --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,3 @@ +pub mod schema; +pub mod structs; +pub mod methods; diff --git a/src/database/schema.rs b/src/database/schema.rs new file mode 100644 index 0000000..9c89493 --- /dev/null +++ b/src/database/schema.rs @@ -0,0 +1,10 @@ +table! { + form (id) { + id -> Integer, + created_at -> Timestamp, + lastvisit_at -> Timestamp, + token -> Text, + nc_username -> Text, + nc_password -> Text, + } +} diff --git a/src/database/structs.rs b/src/database/structs.rs new file mode 100644 index 0000000..1e699a9 --- /dev/null +++ b/src/database/structs.rs @@ -0,0 +1,16 @@ +//use diesel::{self, prelude::*}; +use chrono::NaiveDateTime; + +use crate::database::schema::form; +//use crate::config::CONFIG; + +#[table_name = "form"] +#[derive(Serialize, Queryable, Insertable, Debug, Clone)] +pub struct Form { + pub id: i32, + pub created_at: NaiveDateTime, + pub lastvisit_at: NaiveDateTime, + pub token: String, + pub nc_username: String, + pub nc_password: String, +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..8c7a49e --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,45 @@ +use crate::templates::TplError; + +use std::fmt; +use actix_web::dev::HttpResponseBuilder; +use actix_web::{error, http::header, http::StatusCode, HttpResponse}; +use askama::Template; + +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 { + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/forward.rs b/src/forward.rs new file mode 100644 index 0000000..ec02821 --- /dev/null +++ b/src/forward.rs @@ -0,0 +1,265 @@ +use actix_web::client::{Client, ClientRequest}; +use actix_web::{http, web, HttpRequest, HttpResponse}; +use askama::Template; +use chrono::Utc; +use url::Url; + +use crate::account::*; +use crate::config::PAYLOAD_LIMIT; +use crate::database::methods::InsertableForm; +use crate::database::structs::Form; +use crate::debug; +use crate::sniff::*; +use crate::templates::*; +use crate::DbPool; +use crate::CONFIG; +use crate::errors::{TrainCrash, crash}; + +pub async fn forward( + req: HttpRequest, + body: web::Bytes, + url: web::Data, + client: web::Data, +) -> Result { + let route = req.uri().path(); + + // if check_route returns true, + // the user supposedly tried to access a restricted page. + // They get redirected to the main page. + if check_route(route) { + debug(&format!("Restricted route blocked: {}", route)); + return Ok(web_redir("/")); + } + + 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 + 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()); + } + + // 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(route, &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("/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())); + } + + // check the response before returning it + if check_response(route, &response_body) { + return Ok(web_redir("/")); + } + Ok(client_resp.body(response_body)) +} + +#[derive(Deserialize)] +pub struct LoginToken { + pub token: String, +} + +pub async fn forward_login( + req: HttpRequest, + params: web::Path, + client: web::Data, + dbpool: web::Data, +) -> Result { + // if the user is already logged in, redirect to the Forms app + if is_logged_in(&req).is_some() { + return Ok(web_redir("/apps/forms/")); + } + + // check if the provided token seems valid. If not, early return. + if !check_token(¶ms.token) { + debug("Incorrect admin token given."); + return Err(crash(get_lang(&req), "error_dirtyhacker")); + } + + let conn = dbpool + .get() + .map_err(|e| { + eprintln!("error_forwardlogin_db: {}", e); + crash(get_lang(&req), "error_forwardlogin_db") + })?; + + // check if the link exists in DB. if it does, update lastvisit_at. + let formdata = Form::get_from_token(¶ms.token, &conn).map_err(|e| { + eprintln!("error_forwardlogin_db_get (diesel error): {}", e); + crash(get_lang(&req), "error_forwardlogin_db_get") + })?.ok_or_else(|| { + eprintln!("error_forwardlogin_db_get (none error)"); + crash(get_lang(&req), "error_forwardlogin_db_get") + })?; + + // else, try to log the user in with DB data, then redirect. + login(&client, &req, &formdata.nc_username, &formdata.nc_password).await +} + +// creates a NC account using a random name and password. +// the account gets associated with a token in sqlite DB. +pub async fn forward_register( + req: HttpRequest, + client: web::Data, + dbpool: web::Data, +) -> Result { + let lang = get_lang(&req); + + // if the user is already logged in, redirect to the Forms app + if is_logged_in(&req).is_some() { + return Ok(web_redir("/apps/forms/")); + } + + // if the user has already generated an admin token, redirect too + if let Some(token) = has_admintoken(&req) { + let admin_token = + token.splitn(2, ';').collect::>()[0].replace("sncf_admin_token=", ""); + // sanitize the token beforehand, cookies are unsafe + if check_token(&admin_token) { + return Ok(web_redir(&format!( + "{}/admin/{}", + CONFIG.sncf_url, &admin_token + ))); + } else { + debug("Incorrect admin token given."); + return Err(crash(lang, "error_dirtyhacker")); + } + } + + let nc_username = gen_name(); + let nc_password = gen_token(); + // 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(); + + // store the result in DB + let form_result = Form::insert( + InsertableForm { + created_at: Utc::now().naive_utc(), + lastvisit_at: Utc::now().naive_utc(), + token: &token, + nc_username: &nc_username, + nc_password: &nc_password, + }, + &conn, + ); + + if form_result.is_err() { + return Err(crash(lang, "error_forwardregister_db")) + } + + Ok(HttpResponse::Ok() + .content_type("text/html") + .set_header( + "Set-Cookie", + format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token), + ) + .body( + TplLink { + lang: &lang, + admin_token: &token, + config: &CONFIG, + } + .render() + .map_err(|e| { + eprintln!("error_tplrender (TplLink): {}", e); + crash(lang, "error_tplrender") + })?, + )) +} + +// create a new query destined to the nextcloud instance +// needed to forward any query +fn forge_from( + route: &str, + req: &HttpRequest, + url: &web::Data, + client: &web::Data, +) -> ClientRequest { + let mut new_url = url.get_ref().clone(); + new_url.set_path(route); + new_url.set_query(req.uri().query()); + + // insert forwarded header if we can + let forwarded_req = client.request_from(new_url.as_str(), req.head()); + if let Some(addr) = req.head().peer_addr { + forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) + } else { + forwarded_req + } +} + +fn web_redir(location: &str) -> HttpResponse { + HttpResponse::SeeOther() + .header(http::header::LOCATION, location) + .finish() +} + +pub async fn index(req: HttpRequest) -> Result { + Ok(HttpResponse::Ok().content_type("text/html").body( + TplIndex { + lang: &get_lang(&req), + } + .render() + .map_err(|e| { + eprintln!("error_tplrender (TplIndex): {}", e); + crash(get_lang(&req), "error_tplrender") + })?, + )) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3467f39 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +#[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_web::client::Client; +use actix_web::{middleware, web, App, FromRequest, HttpServer}; +use actix_files::Files; +use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager}; +use url::Url; + +use crate::config::CONFIG; +use crate::config::PAYLOAD_LIMIT; +use crate::forward::*; + +mod config; +mod database; +mod forward; +mod sniff; +mod account; +mod templates; +mod errors; + +type DbPool = r2d2::Pool>; + +embed_migrations!(); + +#[actix_rt::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!("Opening database {}", CONFIG.database_path); + let manager = ConnectionManager::::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()) + /*.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::get().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); + } +} diff --git a/src/sniff.rs b/src/sniff.rs new file mode 100644 index 0000000..cfe3c59 --- /dev/null +++ b/src/sniff.rs @@ -0,0 +1,86 @@ +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 { + "/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 +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 +pub fn check_new_form(route: &str, body: &web::Bytes) -> u64 { + let req = String::from_utf8_lossy(body); + + let new_form_route = "/apps/forms/api/v1/form"; + + if route != new_form_route { + return 0; + } + + // 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["id"] != Value::Null && v["isAnonymous"] == Value::Null { + v["id"].as_u64().unwrap_or_else(|| { + eprintln!("check_new_form: failed to parse formid: {}", v); + 0 + }) + } else { + 0 + } +} + +const BLOCKED_ROUTES: &'static [&'static str] = &[ + "/apps/settings", + "/login", + "/settings", + "/ocs/v", + "/remote.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) { + return true; + } + } + false +} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..2825677 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,62 @@ +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 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(); + } + } + String::from("en") +} + +mod filters { + use crate::config::LOC; + + pub fn tr(key: &str, lang: &str) -> askama::Result { + Ok(String::from( + LOC.get(key) + .ok_or_else(|| { + eprintln!("tr filter: couldn't find the key {}", key); + askama::Error::from(std::fmt::Error) + })? + .get(lang) + .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) + })?, + )) + } +} diff --git a/templates/assets/Ubuntu-R.ttf b/templates/assets/Ubuntu-R.ttf new file mode 100644 index 0000000..d748728 Binary files /dev/null and b/templates/assets/Ubuntu-R.ttf differ diff --git a/templates/assets/cloud.css b/templates/assets/cloud.css new file mode 100644 index 0000000..5685b6f --- /dev/null +++ b/templates/assets/cloud.css @@ -0,0 +1,140 @@ +.has-text-centered > * { + text-align: center; +} + +.c-subelem, .c-fullwidth > * { + color: #2c2c2c; +} + +.c-blue { +} + +.c-blue > a { + color: white; + background: #1f58c6; +} + +.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: 2px; + text-align: center; + transition: all .2s ease-in-out; + white-space: nowrap; + cursor: pointer; + text-decoration: none; + padding: 1rem; + width: max-content; + margin: 0.5rem; +} + +.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); + } +} diff --git a/templates/assets/favicon.ico b/templates/assets/favicon.ico new file mode 100644 index 0000000..2ac73a7 Binary files /dev/null and b/templates/assets/favicon.ico differ diff --git a/templates/assets/index-background.png b/templates/assets/index-background.png new file mode 100644 index 0000000..6edb3d5 Binary files /dev/null and b/templates/assets/index-background.png differ diff --git a/templates/assets/index.css b/templates/assets/index.css new file mode 100644 index 0000000..213062e --- /dev/null +++ b/templates/assets/index.css @@ -0,0 +1,230 @@ +@font-face { + font-family: 'Ubuntu-R'; + src: url('/assets/Ubuntu-R.ttf'); + font-weight: normal; + font-style: normal; +} + +* { + 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: white; + 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: 1.75vw; +} + +p { + font-size: 1.25vw; + line-height: 1.6; +} + +.beta-tag { + background: #f47606; + color: white; + border-radius: 5px; + font-size: 0.9rem; + padding: 0.3rem; + margin-left: 0.5rem; +} +.beta-banner a { + color: #ffeb7f; +} + +.beta-banner { + background: repeating-linear-gradient( 45deg, #d56009, #d56009 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; + +} + +.page-heading > p { + color: white; +} + +.page-heading > p > a { + color: #c3cce8; +} + +.page-heading.error { + background: url("/assets/index-background.png"), linear-gradient(0deg, #790000 0%, #a40000 100%) +} + +.ncstyle-button.error { + background: #ee4040; +} + +.navbar { + height: 50px; +} + +body, html { + margin: 0; + padding: 0; +} + +.ncstyle-button { + color: #FFF; + 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: max-content; + line-height: 2.25rem; + padding: 2vh 3vw; + background: #2a87ff; + font-size: 1.5vw; + min-width: 18vw; + display: block; + transition: all .25s ease-in-out; +} + +.margin-bottom { + margin-bottom: 1rem; +} + +.ncstyle-button:hover { + background: #2478e3; +} + +.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: 50vw; +} + +#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 { + 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; } +} diff --git a/templates/assets/logo.svg b/templates/assets/logo.svg new file mode 100644 index 0000000..52f2494 --- /dev/null +++ b/templates/assets/logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/assets/screen-fields.png b/templates/assets/screen-fields.png new file mode 100644 index 0000000..817e483 Binary files /dev/null and b/templates/assets/screen-fields.png differ diff --git a/templates/assets/screen-formslist.png b/templates/assets/screen-formslist.png new file mode 100644 index 0000000..2c4b906 Binary files /dev/null and b/templates/assets/screen-formslist.png differ diff --git a/templates/assets/screen-params.png b/templates/assets/screen-params.png new file mode 100644 index 0000000..8ac4e2b Binary files /dev/null and b/templates/assets/screen-params.png differ diff --git a/templates/assets/screen-question.png b/templates/assets/screen-question.png new file mode 100644 index 0000000..d582654 Binary files /dev/null and b/templates/assets/screen-question.png differ diff --git a/templates/assets/screen-responses-export.png b/templates/assets/screen-responses-export.png new file mode 100644 index 0000000..aeac552 Binary files /dev/null and b/templates/assets/screen-responses-export.png differ diff --git a/templates/assets/screen-responses.png b/templates/assets/screen-responses.png new file mode 100644 index 0000000..caafd8d Binary files /dev/null and b/templates/assets/screen-responses.png differ diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..94b92e1 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,28 @@ + + + + {{ "error_title"|tr(lang) }} + + + + + + + + +
+
+
+

{{ "error_title"|tr(lang) }}

+

{{ "error_description"|tr(lang) }}

+

{{ error_msg|tr(lang) }}

+

{{ "error_note1"|tr(lang) }} +

{{ "error_note2"|tr(lang) }} +

+
+ +
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..1890c0c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,118 @@ + + + + {{ "index_title"|tr(lang) }} – {{ "index_description"|tr(lang) }} + + + + + + + + +
+
+
+ +
+
+

{{ "index_title"|tr(lang) }}{{ "index_beta_tag"|tr(lang) }}

+

{{ "index_description"|tr(lang) }}

+
+
+ + +
+
+

{{ "index_beta_banner_desc1"|tr(lang) }}

+

{{ "index_beta_banner_desc1"|tr(lang) }}

+

{{ "index_beta_banner_desc2"|tr(lang) }}{{ "index_beta_banner_desc_link"|tr(lang) }}.

+
+
+
+
+
+
+
+

{{ "index_disclaimer1"|tr(lang) }}

+

{{ "index_disclaimer2"|tr(lang) }}{{ "index_disclaimer2_link_org"|tr(lang) }}{{ "index_disclaimer2_or"|tr(lang) }}{{ "index_disclaimer2_nc"|tr(lang) }}.

+
+
+
+
+
+
+ +
+
+

{{ "index_panel1_title"|tr(lang) }}

+

{{ "index_panel1_desc1"|tr(lang) }}

+

{{ "index_panel1_desc2"|tr(lang) }}

+
+
+
+
+ +
+
+

{{ "index_panel2_title"|tr(lang) }}

+

{{ "index_panel2_desc1"|tr(lang) }}

+

{{ "index_panel2_desc2"|tr(lang) }}{{ "index_panel2_desc2_link"|tr(lang) }}.

+
+
+
+
+ +
+
+

{{ "index_panel3_title"|tr(lang) }}

+

{{ "index_panel3_desc1"|tr(lang) }}

+
+
+
+
+ +
+
+

{{ "index_panel4_title"|tr(lang) }}

+

{{ "index_panel4_desc1"|tr(lang) }}

+
+
+
+
+ +
+
+

{{ "index_panel5_title"|tr(lang) }}

+

{{ "index_panel5_desc1"|tr(lang) }}

+

{{ "index_panel5_desc2"|tr(lang) }}

+
+
+
+
+ +
+
+

{{ "index_panel6_title"|tr(lang) }}

+

{{ "index_panel5_desc1"|tr(lang) }}

+
+
+
+
+ +
+
+
+

Crédits

+

{{ "index_credits_desc1"|tr(lang) }}{{ "index_credits_desc1_link"|tr(lang) }}{{ "index_credits_desc1_a"|tr(lang) }}

+

{{ "index_credits_desc2"|tr(lang) }}Neil{{ "index_credits_desc2_for"|tr(lang) }}{{ "index_credits_desc2_org"|tr(lang) }} {{"index_credits_desc3"|tr(lang) }}.

+
+
+ + diff --git a/templates/link.html b/templates/link.html new file mode 100644 index 0000000..63e396c --- /dev/null +++ b/templates/link.html @@ -0,0 +1,64 @@ + + + + {{ "link_title"|tr(lang) }} – {{ "index_title"|tr(lang) }} + + + + + + + + + + +
+
+
+
+
+

{{ "link_title"|tr(lang) }}

+

{{ "link_desc1"|tr(lang)|safe }}

+

{{ "link_desc2"|tr(lang)|safe }}

+
+
+ +
+
+ +

{{ "link_desc3"|tr(lang) }}

+
+ +
+

{{ "link_note"|tr(lang) }}{{ config.prune_days }}{{ "link_note2"|tr(lang) }}

+
+
+
+
+ +