#### Imports from io import BytesIO import re import os import jinja2 as j2 from flask import ( Flask, render_template, render_template_string, request, redirect, flash, send_file, ) from markupsafe import Markup # No longer imported from Flask # Form libraries from flask_wtf import FlaskForm from flask_bootstrap import Bootstrap from wtforms import ( StringField, RadioField, SelectField, SubmitField, TextAreaField, BooleanField, SelectMultipleField, ) from wtforms.validators import ( DataRequired, InputRequired, Optional, Regexp, URL, Length, ) # Markdown to HTML - for debugging purposes from misaka import Markdown, HtmlRenderer # Managing zipfiles import zipfile from flask_cors import CORS from urllib import parse from secrets import token_urlsafe #### GLOBAL VARIABLES YOLOGEN_VERSION = "0.8.1" GENERATOR_DICT = {"GENERATOR_VERSION": YOLOGEN_VERSION} #### Create FLASK and Jinja Environments app = Flask(__name__) Bootstrap(app) app.config["SECRET_KEY"] = token_urlsafe(16) # Necessary for the form CORS cors = CORS(app) environment = j2.Environment(loader=j2.FileSystemLoader("templates/")) #### Custom functions # Define custom filter @app.template_filter("render_markdown") def render_markdown(text): renderer = HtmlRenderer() markdown = Markdown(renderer) return markdown(text) # Add custom filter j2.filters.FILTERS["render_markdown"] = render_markdown # Converting markdown to html def markdown_file_to_html_string(file): with open(file, "r") as file: markdown_content = file.read() # Convert content from Markdown to HTML html_content = render_markdown(markdown_content) # Return Markdown and HTML contents return markdown_content, html_content ### Forms class GeneralInfos(FlaskForm): app_id = StringField( Markup( "Identifiant (id) de l'application" ), description="En minuscule et sans espace.", validators=[DataRequired(), Regexp("[a-z_1-9]+.*(? \ N'indiquez pas de titre du logiciel au début, car ce sera intégré dans une sous-partie "Overview" '''), validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) disclaimer = TextAreaField( "Saisissez le contenu du fichier DISCLAIMER.md, qui liste des avertissements et points d'attention.", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) pre_install = TextAreaField( "Saisissez le contenu du fichier PRE_INSTALL.md", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) post_install = TextAreaField( "Saisissez le contenu du fichier POST_INSTALL.md", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) pre_upgrade = TextAreaField( "Saisissez le contenu du fichier PRE_UPGRADE.md", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) post_upgrade = TextAreaField( "Saisissez le contenu du fichier POST_UPGRADE.md", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) admin = TextAreaField( "Saisissez le contenu du fichier ADMIN.md", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) class MoreAdvanced(FlaskForm): enable_change_url = BooleanField( "Gérer le changement d'URL d'installation (script change_url)", default=True, render_kw={ "title": "Faut-il permettre le changement d'URL pour l'application ? (fichier change_url)" }, ) use_logrotate = BooleanField( "Utiliser logrotate pour les journaux de l'app", default=True, render_kw={ "title": "Si l'application genère des journaux (log), cette option permet d'en gérer l'archivage. Recommandé." }, ) # TODO : specify custom log file # custom_log_file = "/var/log/$app/$app.log" "/var/log/nginx/${domain}-error.log" use_fail2ban = BooleanField( "Protéger l'application des attaques par force brute (via fail2ban)", default=False, render_kw={ "title": "Si l'application genère des journaux (log) d'erreurs de connexion, cette option permet de bannir automatiquement les IP au bout d'un certain nombre d'essais de mot de passe. Recommandé." }, ) use_cron = BooleanField( "Ajouter une tâche CRON pour cette application", description="Corresponds à des opérations périodiques de l'application", default=False, ) cron_config_file = TextAreaField( "Saisissez le contenu du fichier CRON", validators=[Optional()], render_kw={ "class": "form-control", "spellcheck": "false", }, ) fail2ban_regex = StringField( "Expression régulière pour fail2ban", # Regex to match into the log for a failed login validators=[Optional()], render_kw={ "placeholder": "A regular expression", "class": "form-control", "title": "Expression régulière à vérifier dans le journal pour que fail2ban s'active (cherchez une ligne qui indique une erreur d'identifiants deconnexion).", }, ) ## Main form class GeneratorForm( GeneralInfos, IntegrationInfos, UpstreamInfos, InstallQuestions, Resources, SpecificTechnology, AppConfig, Documentation, MoreAdvanced ): class Meta: csrf = False generator_mode = SelectField( "Mode du générateur", description="En mode tutoriel, l'application générée contiendra des commentaires additionnels pour faciliter la compréhension. En version épurée, l'application générée ne contiendra que le minimum nécessaire.", choices=[("simple", "Version épurée"), ("tutorial", "Version tutoriel")], default="true", validators=[DataRequired()], ) submit_preview = SubmitField("Prévisualiser") submit_download = SubmitField("Télécharger le .zip") submit_demo = SubmitField('Remplir avec des valeurs de démonstration', render_kw={"onclick": "fillFormWithDefaultValues()", "title": "Générer une application minimaliste complète et fonctionnelle à partir de laquelle itérer" }) #### Web pages @app.route("/", methods=["GET", "POST"]) def main_form_route(): main_form = GeneratorForm() app_files = [] if request.method == "POST": if not main_form.validate_on_submit(): print("Form not validated?") print(main_form.errors) return render_template( "index.html", main_form=main_form, generator_info=GENERATOR_DICT, generated_files={} ) if main_form.submit_preview.data: submit_mode = "preview" elif main_form.submit_demo.data: submit_mode = "demo" # TODO : for now this always trigger a preview. Not sure if that's an issue else: submit_mode = "download" class AppFile: def __init__(self, id_, destination_path=None): self.id = id_ self.destination_path = destination_path self.content = None app_files = [ AppFile("manifest", "manifest.toml"), AppFile("_common.sh", "scripts/_common.sh"), AppFile("install", "scripts/install"), AppFile("remove", "scripts/remove"), AppFile("backup", "scripts/backup"), AppFile("restore", "scripts/restore"), AppFile("upgrade", "scripts/upgrade"), AppFile("nginx", "conf/nginx.conf"), ] if main_form.enable_change_url.data: app_files.append(AppFile("change_url", "scripts/change_url")) if main_form.main_technology.data not in ["none", "php"]: app_files.append(AppFile("systemd", "conf/systemd.service")) # TODO : buggy, tries to open php.j2 # if main_form.main_technology.data == "php": # app_files.append(AppFile("php", "conf/extra_php-fpm.conf")) if main_form.description.data: app_files.append(AppFile("DESCRIPTION", "docs/DESCRIPTION.md")) if main_form.disclaimer.data: app_files.append(AppFile("DISCLAIMER", "docs/DISCLAIMER.md")) if main_form.pre_install.data: app_files.append(AppFile("PRE_INSTALL", "docs/PRE_INSTALL.md")) if main_form.post_install.data: app_files.append(AppFile("POST_INSTALL", "docs/POST_INSTALL.md")) if main_form.pre_upgrade.data: app_files.append(AppFile("PRE_UPGRADE", "docs/PRE_UPGRADE.md")) if main_form.post_upgrade.data: app_files.append(AppFile("POST_UPGRADE", "docs/POST_UPGRADE.md")) if main_form.admin.data: app_files.append(AppFile("ADMIN", "docs/ADMIN.md")) template_dir = os.path.dirname(__file__) + "/templates/" for app_file in app_files: template = open(template_dir + app_file.id + ".j2").read() app_file.content = render_template_string(template, data=dict(request.form | GENERATOR_DICT)) app_file.content = re.sub(r'\n\s+$', '\n', app_file.content, flags=re.M) app_file.content = re.sub(r'\n{3,}', '\n\n', app_file.content, flags=re.M) # TODO #if main_form.use_custom_config_file: # app_files.append(AppFile("appconf", "conf/" + main_form.custom_config_file)) # app_files[-1].content = main_form.custom_config_file_content # TODO : same for cron job if submit_mode == "download": # Generate the zip file f = BytesIO() with zipfile.ZipFile(f, "w") as zf: print("Exporting zip archive for app: " + request.form["app_id"]) for app_file in app_files: print(app_file.id) zf.writestr(app_file.destination_path, app_file.content) f.seek(0) # Send the zip file to the user return send_file(f, as_attachment=True, download_name=request.form["app_id"] + ".zip") return render_template( "index.html", main_form=main_form, generator_info=GENERATOR_DICT, generated_files=app_files, ) #### Running the web server if __name__ == "__main__": app.run(debug=True)