From 6828c5684cefae03d4e6828013af8cc9d8bd47fe Mon Sep 17 00:00:00 2001 From: Adrian Amaglio Date: Thu, 30 Apr 2020 00:07:36 +0200 Subject: [PATCH] v2 --- Dockerfile | 17 +++ list.tpl | 11 ++ main.py | 323 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 4 files changed, 354 insertions(+) create mode 100755 Dockerfile create mode 100644 list.tpl create mode 100755 main.py create mode 100755 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..27c07e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:alpine + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +RUN apk add python3-dev build-base linux-headers pcre-dev +RUN pip install uwsgi + +ENV UID=0 +ENV MOUNT=/ + +COPY ./main.py ./list.tpl ./ + +# I juste wanted to change the socket owner but it turned out I needed to change thu uwsgi user +#CMD uwsgi --exec-asap 'chown $UID:$UID /tmp/uwsgi/ ; mkdir -p $BASE_PATH && chown $UID:$UID $BASE_PATH' -s /tmp/uwsgi/uwsgi.sock --uid $UID --manage-script-name --mount /=server:app +CMD uwsgi --chown-socket $UID -s /tmp/uwsgi/uwsgi.sock --mount $MOUNT=main:app diff --git a/list.tpl b/list.tpl new file mode 100644 index 0000000..655ac70 --- /dev/null +++ b/list.tpl @@ -0,0 +1,11 @@ + + + diff --git a/main.py b/main.py new file mode 100755 index 0000000..9f6abe5 --- /dev/null +++ b/main.py @@ -0,0 +1,323 @@ +import bottle +request = bottle.request +response = bottle.response +redirect = bottle.redirect +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import os +import re +import pymongo +from dotenv import load_dotenv +import random, string +import html # for sanitization + + +# Load file from .env file. +load_dotenv(os.path.dirname(__file__) + '.env') + +token_chars = string.ascii_lowercase+string.ascii_uppercase+string.digits +token_len = 50 + + +# The exception that is thrown when an argument is missing +class MissingParameterException (Exception): + pass + +app = application = bottle.Bottle() + +# Get address and port from env +listen_address = os.environ['LISTEN_ADDRESS'] if 'LISTEN_ADDRESS' in os.environ else '0.0.0.0' +listen_port = os.environ['LISTEN_PORT'] if 'LISTEN_PORT' in os.environ else 8080 + +# Get mail related informations from env +mail_default_subject = os.environ['MAIL_DEFAULT_SUBJECT'] if 'MAIL_DEFAULT_SUBJECT' in os.environ else 'Nouveau message' +mail_subject_prefix = os.environ['MAIL_SUBJECT_PREFIX'] if 'MAIL_SUBJECT_PREFIX' in os.environ else '[Contact]' + +# Redirect info +success_redirect_default = os.environ['SUCCESS_REDIRECT_DEFAULT'] if 'SUCCESS_REDIRECT_DEFAULT' in os.environ else '/success' +failure_redirect_default = os.environ['FAILURE_REDIRECT_DEFAULT'] if 'FAILURE_REDIRECT_DEFAULT' in os.environ else '/fail' + +# Get SMTP infos from env +if 'SMTP_SERVER_ADDRESS' in os.environ: + smtp_server_address = os.environ['SMTP_SERVER_ADDRESS'] +else: + raise MissingParameterException("Environment variable SMTP_SERVER_ADDRESS is missing") + +if 'SMTP_SERVER_PORT' in os.environ: + smtp_server_port = os.environ['SMTP_SERVER_PORT'] +else: + raise MissingParameterException("Environment variable SMTP_SERVER_PORT is missing") + +if 'SMTP_SERVER_USERNAME' in os.environ: + smtp_server_username = os.environ['SMTP_SERVER_USERNAME'] +else: + raise MissingParameterException("Environment variable SMTP_SERVER_USERNAME is missing") + +if 'SMTP_SERVER_PASSWORD' in os.environ: + smtp_server_password = os.environ['SMTP_SERVER_PASSWORD'] +else: + raise MissingParameterException("return Environment variable SMTP_SERVER_PASSWORD is missing") + +if 'SMTP_SERVER_SENDER' in os.environ: + smtp_server_sender = os.environ['SMTP_SERVER_SENDER'] +else: + raise MissingParameterException("Environment variable SMTP_SERVER_SENDER is missing") + +# Get mongodb connection +if 'MONGODB_HOST' in os.environ: + mongodb_host = os.environ['MONGODB_HOST'] +else: + raise MissingParameterException("Environment variable MONGODB_HOST is missing") + +mongodb_port = os.environ['MONGODB_PORT'] if 'MONGODB_PORT' in os.environ else '27017' +mongodb_dbname = os.environ['MONGODB_DBNAME'] if 'MONGODB_DBNAME' in os.environ else 'contact_mailer' + +# Security +if 'SMTP_SSL' in os.environ and os.environ['SMTP_SSL'] == 'true': + security = 'ssl' +elif 'SMTP_STARTTLS' in os.environ and os.onviron['SMTP_STARTTLS'] == 'true': + security = 'starttls' +else: + raise MissingParameterException('No security env var (SMTP_SSL or SMTP_STARTTLS) have been defined. (Expected true or false)') + +if 'ADMIN_PASSWORD' in os.environ: + admin_password = os.environ['ADMIN_PASSWORD'] +else: + raise MissingParameterException("Environment variable ADMIN_PASSWORD is missing") + + +# mongodb initialization +mongodb_client = pymongo.MongoClient("mongodb://{}:{}/".format(mongodb_host, mongodb_port)) +mongodb_database = mongodb_client[mongodb_dbname] + +# form template regex +form_regex = '\{\{(\w+)(\|\w+)?\}\}' + + +@app.post('/submit') +@app.post('/submit/') +def submission (): + # Getting subject + if 'token' in request.forms: + token = request.forms.getunicode('token') + else: + response.status = 400 + return 'Le jeton d’autentification est requis' + if 'mail' in request.forms: + from_address = request.forms.getunicode('mail') + else: + response.status = 400 + return 'Le mail est requis' + + try: + form = mongodb_database['forms'].find({'token': token})[0] + except IndexError as e: + response.status = 400 + return 'L’authentification a échouée' + + try: + subject_fields = fill_fields(request, get_fields(form['subject'])) + content_fields = fill_fields(request, get_fields(form['content'])) + except MissingParameterException as e: + response.status = 404 + return str(e) + + subject = re.sub(form_regex, r'{\1}', form['subject']).format(**subject_fields) + content = re.sub(form_regex, r'{\1}', form['content']).format(**content_fields) + + if not send_mail(from_address, form['mail'], subject, content): + response.status = 500 + return 'Le mail n’a pas pu être envoyé.' + + # Redirection + #redirect(success_redirect_default) + return 'Mail envoyé !' + +def get_fields (string): + """ Parse the string looking for template elements and create an array with template to fill and their default values. None if mandatory. """ + result = {} + for match in re.findall(form_regex, string): + result[match[0]] = match[1] + return result + +def fill_fields(request, fields): + """Look for fields in request and fill fields dict with values or let default ones. If the value is required, throw exception.""" + for field in fields: + if field in request.forms: + fields[field] = html.escape(request.forms[field]) + elif fields[field] == None: + raise MissingParameterException("Le champs {} est obligatoire".format(field)) + return fields + +def send_mail(from_address, to, subject, content): + """Actually connect to smtp server, build a message object and send it as a mail""" + msg = MIMEMultipart() + msg['From'] = smtp_server_sender + msg.add_header('reply-to', from_address) + msg['To'] = to + msg['Subject'] = subject + msg.attach(MIMEText(content, 'plain', "utf-8")) + + # SMTP preambles + if security == 'ssl': + smtp = smtplib.SMTP_SSL(smtp_server_address, smtp_server_port) + elif security == 'starttls': + smtp = smtplib.SMTP(smtp_server_address, smtp_server_port) + smtp.ehlo() + if security == 'starttls': + smtp.starttls() + smtp.ehlo() + + # SMTP connection + smtp.login(smtp_server_username, smtp_server_password) + refused = smtp.sendmail(smtp_server_sender, to, msg.as_string()) + smtp.close() + if refused: + print('Message was not send to ' + str(refused)) + return False + return True + + +@app.post('/form') +@app.post('/form/') +def create_form (): + # Getting subject template + if 'subject' in request.forms: + subject = request.forms.getunicode('subject') + elif mail_default_subject != '': + subject = mail_default_subject + else: + response.status = 400 + return 'Le champs « sujet » est requis' + + # Getting mail content + if 'content' in request.forms: + content = request.forms.getunicode('content') + else: + response.status = 400 + return 'Le champs « contenu » est requis' + + # Getting from address + if 'mail' in request.forms: + mail = request.forms.getunicode('mail') + else: + response.status = 400 + return 'Le champs « adresse » est requis' + + # Getting auth token + if 'token' in request.forms: + token = request.forms.getunicode('token') + else: + response.status = 400 + return 'Le jeton d’autentification n’a pas été envoyé' + + try: + user = mongodb_database['users'].find({'token': token})[0] + except IndexError as e: + response.status = 400 + return 'L’authentification a échouée' + + # TODO limit the insertion rate + token = ''.join(random.sample(token_chars, token_len)) + inserted = mongodb_database['forms'].insert_one({ + 'mail': mail, + 'content': content, + 'subject': subject, + 'user_id': user['_id'], + 'token': token, + }) + + return 'Créé : ' + token + + + +##################################################### Admin ############################################$ +@app.post('/admin/list') +@app.post('/admin/list/') +def admin_list (): + if not ('admin_pass' in request.forms and request.forms['admin_pass'] == admin_password): + response.status = 400 + return 'Le champs « admin_pass » est requis' + return bottle.template("list.tpl", mongodb_database=mongodb_database) + + +@app.put('/user/') +@app.put('/user//') +def create_user (username): + if not ('admin_pass' in request.forms and request.forms['admin_pass'] == admin_password): + response.status = 400 + return 'Le champs « admin_pass » est requis' + try: + mongodb_database['users'].find({'username': username})[0] + return 'L’utilisateur existe déjà' + except IndexError as e: + pass + inserted = mongodb_database['users'].insert_one({ + 'username': username, + 'token': ''.join(random.sample(token_chars, token_len)) + }) + return 'Créé : ' + username + + +@app.delete('/user/') +@app.delete('/user//') +def delete_user (username): + if not ('admin_pass' in request.forms and request.forms['admin_pass'] == admin_password): + response.status = 400 + return 'Le champs « admin_pass » est requis' + try: + mongodb_database['users'].find({'username': username})[0] + except IndexError as e: + response.status = 400 + return 'L’utilisateur n’existe pas' + mongodb_database['users'].delete_one({ + 'username': username, + }) + return 'Supprimé ' + username + + +@app.delete('/form/') +@app.delete('/form//') +def delete_form(token): + # If admin or form owner + admin = False + if 'admin_pass' in request.forms and request.forms['admin_pass'] == admin_password: + admin = True + + user_token = False + if 'token' in request.forms: + try: + user = mongodb_database['users'].find({'token':request.forms['token']})[0] + user_token = True + except IndexError as e: + pass + + # Actually delete + try: + form = mongodb_database['forms'].find({'token':token })[0] + except IndexError as e: + response.status = 400 + return 'Le token n’est pas valide' + + if (user_token and form['user_id'] == user['_id']) or admin: + mongodb_database['forms'].delete_one({ + 'token': token, + }) + return 'Supprimé ' + token + response.status = 400 + return 'Vous n’avez pas les droits pour supprimer ce formulaire' + + +class StripPathMiddleware(object): + ''' + Get that slash out of the request + ''' + def __init__(self, a): + self.a = a + def __call__(self, e, h): + e['PATH_INFO'] = e['PATH_INFO'].rstrip('/') + return self.a(e, h) + +if __name__ == '__main__': + bottle.run(app=StripPathMiddleware(app), host=listen_address, port=listen_port, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..b7370ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +bottle==0.12.18 +pymongo==3.10.1 +python-dotenv==0.13.0