From 5cc68b7d0a9507847f8cc75fddda180b2c6410a2 Mon Sep 17 00:00:00 2001 From: Andrew Dinh Date: Sun, 16 May 2021 22:00:00 -0700 Subject: [PATCH] first commit --- .env.example | 2 ++ Dockerfile | 9 ++++++ README.md | 26 ++++++++++++++++ docker-compose.yml | 19 +++++++++++ main.py | 71 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 ++ templates/index.html | 36 +++++++++++++++++++++ templates/message.html | 24 ++++++++++++++ 8 files changed, 189 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 templates/index.html create mode 100644 templates/message.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d7979a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +HTTP_ENDPOINT=https://example.invalid +ESCAPE_HTML=False \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2234e7e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM tiangolo/uwsgi-nginx-flask:python3.8 +LABEL maintainer="Andrew Dinh " +LABEL version="0.1.0" + +EXPOSE 8888 + +WORKDIR /app +ADD . . +RUN python3 -m pip install --no-cache-dir -r /app/requirements.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54875e7 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Simple Contact + +Extremely simple contact form with a CAPTCHA. Entries are sent to the specified HTTP endpoint. + +## Building + +1. Install `git`, `docker`, and `docker-compose` +2. + +```bash +git clone https://github.com/andrewkdinh/simple-contact.git +git clone https://github.com/daniel-e/rust-captcha.git +cd simple-contact +cp .env.example .env +# Edit .env +docker-compose up -d +``` +3. Visit `http://localhost:8672` + +## Credits + +- Built with Python, Flask, Docker, Rust CAPTCHA, and water.css + +Mirrors: [GitHub](https://github.com/andrewkdinh/simple-contact) (main), [Gitea](https://gitea.andrewkdinh.com/andrewkdinh/simple-contact) + +Licensed under [AGPL 3.0](./LICENSE) | Copyright (c) 2021 Andrew Dinh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fc297a6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '2.0' +services: + simple-contact: + container_name: simple-contact + build: + context: . + restart: unless-stopped + ports: + - 8672:80 + depends_on: + - rust-captcha + environment: + - HTTP_ENDPOINT=${HTTP_ENDPOINT} # Send messages to this endpoint + - ESCAPE_HTML=${ESCAPE_HTML} # Whether the contents of the message should be escaped for HTML + rust-captcha: + container_name: rust-captcha + build: + context: ../rust-captcha/docker + restart: unless-stopped diff --git a/main.py b/main.py new file mode 100644 index 0000000..6643083 --- /dev/null +++ b/main.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +from flask import Flask, render_template, redirect, request +import requests +from typing import List +import os + +app = Flask(__name__) + +HTTP_ENDPOINT = os.getenv('HTTP_ENDPOINT') +ESCAPE_HTML = os.getenv('ESCAPE_HTML') +if not HTTP_ENDPOINT: + print("ERROR: HTTP_ENDPOINT not set") + exit(1) + +if not ESCAPE_HTML: + print("ERROR: ESCAPE_HTML not set") + exit(1) +if ESCAPE_HTML == "True": + ESCAPE_HTML = True +elif ESCAPE_HTML == "False": + ESCAPE_HTML = False +else: + print("ERROR: ESCAPE_HTML must be set to True or False") + exit(1) + +@app.route("/", methods=["GET", "POST"]) +def index(): + try: + if request.method == "GET": + captcha_id, captcha_png = captcha_get(ttl=600) + return render_template("index.html", captcha_id = captcha_id, captcha_png = captcha_png) + elif request.method == "POST": + captcha_id = request.form.get('captcha-id') + captcha_solution = request.form.get('captcha-solution') + success, trials_left = captcha_validate(captcha_id, captcha_solution) + if not success: + return render_template('message.html', message = "Failed captcha", attempts_left = trials_left) + message = request.form.get('message') + if ESCAPE_HTML: + message = message.replace("<", "<").replace(">", ">") + if message != "": + requests.post(HTTP_ENDPOINT, data={'subject': 'New Simple Contact message', 'message': message}) + return render_template('message.html', message = "Your message was sent successfully") + else: + raise TypeError("Invalid method") + except Exception as e: + print(e) + return render_template('message.html', message="Error occurred") + +def captcha_get(max_tries: int = 3, ttl: int = 120, difficulty: str = "medium") -> List[str]: + """ Creates a captcha and returns [id, base64 encoded png] """ + response = requests.post(f"http://rust-captcha:8000/new/{difficulty}/{max_tries}/{ttl}", headers={'X-Client-ID': 'Simple Contact'}).json() + if response["error_code"] != 0: + raise Exception(response) + return [response["result"]["id"], response["result"]["png"]] + +def captcha_validate(captcha_id: str, captcha_solution: str) -> List: + """ Validates a captcha and returns [success, trials_left] """ + if len(captcha_id) > 100 or len(captcha_solution) > 10: + return [False, 0] + response = requests.post(f"http://rust-captcha:8000/solution/{captcha_id}/{captcha_solution}", headers={'X-Client-ID': 'Simple Contact'}).json() + if response["error_code"] != 0: + print(f"http://rust-captcha:8000/solution/{captcha_id}/{captcha_solution}") + raise Exception(response) + if response["result"]["solution"] == "accepted": + return [True, 0] + return [False, response["result"]["trials_left"]] + +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True, port=8888) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..be8330c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask~=1.1.2 +requests~=2.25.1 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..aac4744 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,36 @@ + + + + + + Simple Contact + + + + + + +

Simple Contact

+
+ +
+ + +
+ +
+ + + +
+ +

Source code available on GitHub and Gitea

+

Licensed under AGPL 3.0 | Styling with water.css

+ + diff --git a/templates/message.html b/templates/message.html new file mode 100644 index 0000000..3b07710 --- /dev/null +++ b/templates/message.html @@ -0,0 +1,24 @@ + + + + + + Simple Contact + + + + + + +

{{ message }}

+ {% if attempts_left %} +

You have {{ attempts_left }} attempt(s) left

+ + {% elif attempts_left == 0 %} +

Captcha is now invalid (reload page after you go back)

+ + {% endif %} + + \ No newline at end of file