14 Commits
v0.1 ... legacy

Author SHA1 Message Date
3fcdaa2b10 Fix formatting of README.md 2020-09-02 21:52:13 -05:00
4309225185 Add Installation section to README.md
- Added Installation section to README.md
- Removed old Usage section
2020-09-02 21:43:31 -05:00
065296f84a Rename functions.py to functions.py.default
Since this is default settings and the user might want to customize
them, functions.py has been renamed. This also will prevent conflicts if
the user has updated their functions.py and then tries to pull.
2020-09-02 18:04:41 -05:00
841bb513d3 Allow easy customization of filename generation
Added a new file called functions.py which contains user-customizable
functions, instead of requiring the user to edit imgupload.py.
2020-09-02 17:14:28 -05:00
f0bb30a747 Change keygen.py to not require root
keygen.py now recommends that you run it as the user you want to have
ownership of secret.key and uploadkeys (such as www-data for nginx).
Then, if uploadkeys or secret.key don't exist, they will be created with
the correct ownership.
2020-09-02 14:26:57 -05:00
7fce3f57e9 Remove secrets from requirements.txt
On macOS, a dependency of secrets fails to install using pip. After
testing, it looks like the secrets module is not required.
2020-08-31 23:46:28 -05:00
a587040809 Bump cryptography from 2.8 to 3.1 (#2) 2020-09-01 04:41:50 +00:00
8a95dbb0fa Remove trailing whitespace from lines 2020-08-31 23:09:39 -05:00
a5a22b7c88 Remove UPLOADKEYS_CHMOD option due to keygen.py
Since keygen.py is run as root, uploadkeys is owned by root. This causes
issues when imgupload.py tries to chmod the uploadkeys file since it
doesn't have permissions to chmod it.
Solution: remove UPLOADKEYS_CHMOD option
2020-08-31 21:14:09 -05:00
7ccaafc6c6 Bugfixes in keygen.py
- Handle if uploadkeys becomes corrupted
- Disambiguate variable names
- Handle case where the uploadkeys file doesn't already exist
2020-08-31 20:32:00 -05:00
797bebb1a1 Fix ENCKEY_PATH check in configtest.py 2020-08-31 20:29:07 -05:00
08f9e13da0 Merge pull request #1 from BBaoVanC/dev
added encryption to uploadkeys and added a key generator
2020-08-31 17:13:24 -07:00
3d1304b3b0 added encryption to uploadkeys and added a key generator 2020-08-31 17:12:39 -07:00
4b624f3fed Ignore settings.py and add settings.py.default 2020-08-31 18:48:22 -05:00
8 changed files with 178 additions and 35 deletions

3
.gitignore vendored
View File

@ -132,3 +132,6 @@ dmypy.json
uploadkeys uploadkeys
savelog.log savelog.log
uwsgi.log uwsgi.log
settings.py
functions.py
secret.key

View File

@ -1,4 +1,26 @@
# imgupload # imgupload
Python Flask uWSGI application to receive and save images over POST requests. ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/BBaoVanC/imgupload/master?color=purple) ![GitHub repo size](https://img.shields.io/github/repo-size/bbaovanc/imgupload?color=purple) ![GitHub All Releases](https://img.shields.io/github/downloads/bbaovanc/imgupload/total?color=purple) ![GitHub issues](https://img.shields.io/github/issues/bbaovanc/imgupload?color=purple) ![GitHub closed issues](https://img.shields.io/github/issues-closed/bbaovanc/imgupload?color=purple) ![GitHub](https://img.shields.io/github/license/bbaovanc/imgupload?color=purple)
This project is still in development. Use at your own risk! ### What is imgupload?
imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests.
### Installation
1. Clone the repository: `git clone https://github.com/BBaoVanC/imgupload.git`
2. Enter the imgupload directory: `cd imgupload`
3. Create a virtualenv: `python3 -m venv env`
4. Enter the virtualenv: `source env/bin/activate`
5. Install dependencies: `python3 -m pip install -r requirements.txt`
6. Run the Flask app
## Running the Flask app
### Using uWSGI
[https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html](https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html)
Instructions specific to imgupload are coming soon
### Using Flask development server
```shell
$ source env/bin/activate # if you haven't already entered the virtualenv
$ export FLASK_APP=imgupload.py
$ flask run
```

View File

@ -9,8 +9,8 @@ defaults = {
"ROOTURL": "https://img.bbaovanc.com/", "ROOTURL": "https://img.bbaovanc.com/",
"SAVELOG": "savelog.log", "SAVELOG": "savelog.log",
"SAVELOG_CHMOD": "0o644", "SAVELOG_CHMOD": "0o644",
"UPLOADKEYS_CHMOD": "0o400",
"SAVELOG_KEYPREFIX": 4, "SAVELOG_KEYPREFIX": 4,
"ENCKEY_PATH": "secret.key"
} }
deftypes = { deftypes = {
@ -19,8 +19,8 @@ deftypes = {
"ROOTURL": str, "ROOTURL": str,
"SAVELOG": str, "SAVELOG": str,
"SAVELOG_CHMOD": int, "SAVELOG_CHMOD": int,
"UPLOADKEYS_CHMOD": int,
"SAVELOG_KEYPREFIX": int, "SAVELOG_KEYPREFIX": int,
"ENCKEY_PATH": str,
} }
@ -94,6 +94,16 @@ if "ROOTURL" in checksettings:
print("[" + u"\u2713" + "] ROOTURL is good!") print("[" + u"\u2713" + "] ROOTURL is good!")
# Check if ENCKEY_PATH exists
enckey_exists = True
if "ENCKEY_PATH" in checksettings:
if not os.path.isfile(settings.ENCKEY_PATH):
enckey_exists = False
print("[!] The path set in ENCKEY_PATH ('{0}') doesn't exist!".format(settings.ENCKEY_PATH))
else:
print("[" + u"\u2713" + "] ENCKEY_PATH exists!")
# Ask the user if SAVELOG is the intended filename # Ask the user if SAVELOG is the intended filename
if "SAVELOG" in checksettings: if "SAVELOG" in checksettings:
print("[*] SAVELOG was interpreted to be {0}".format(settings.SAVELOG)) print("[*] SAVELOG was interpreted to be {0}".format(settings.SAVELOG))
@ -126,6 +136,10 @@ if not uploadfolder_exists:
summarygood = False summarygood = False
print("UPLOAD_FOLDER ({0}) does not exist!".format(settings.UPLOAD_FOLDER)) print("UPLOAD_FOLDER ({0}) does not exist!".format(settings.UPLOAD_FOLDER))
if not enckey_exists:
summarygood = False
print("ENCKEY_PATH ({0}) does not exist!".format(settings.ENCKEY_PATH))
if not rooturl_good: if not rooturl_good:
summarygood = False summarygood = False
print("ROOTURL may cause issues!") print("ROOTURL may cause issues!")

8
functions.py.default Normal file
View File

@ -0,0 +1,8 @@
import string
import random
def generate_name():
chars = string.ascii_letters + string.digits # uppercase, lowercase, and numbers
name = ''.join((random.choice(chars) for i in range(8))) # generate name
return name

View File

@ -1,15 +1,13 @@
from flask import Flask, request, jsonify, abort, Response from flask import Flask, request, jsonify, abort, Response
from cryptography.fernet import Fernet
from flask_api import status from flask_api import status
from pathlib import Path from pathlib import Path
import string
import random import random
import os import os
import datetime import datetime
import settings # app settings (such as allowed extensions) import settings # app settings (such as allowed extensions)
import functions # custom functions
ALPHANUMERIC = string.ascii_letters + string.digits # uppercase, lowercase, and numbers
app = Flask(__name__) # app is the app app = Flask(__name__) # app is the app
@ -21,15 +19,6 @@ def allowed_extension(testext):
return False return False
def generate_name(extension):
namefound = False
while not namefound:
fname = ''.join((random.choice(ALPHANUMERIC) for i in range(8))) + str(extension)
if not Path(fname).is_file():
namefound = True
return fname
def log_savelog(key, ip, savedname): def log_savelog(key, ip, savedname):
if settings.SAVELOG_KEYPREFIX > 0: if settings.SAVELOG_KEYPREFIX > 0:
with open(settings.SAVELOG, "a+") as slogf: with open(settings.SAVELOG, "a+") as slogf:
@ -40,30 +29,31 @@ def log_savelog(key, ip, savedname):
slogf.write("[{0}] {1} - {2}\n".format(datetime.datetime.now(), ip, savedname)) slogf.write("[{0}] {1} - {2}\n".format(datetime.datetime.now(), ip, savedname))
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD) os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
@app.route("/upload", methods = ["POST"]) @app.route("/upload", methods = ["POST"])
def upload(): def upload():
if request.method == "POST": # sanity check: make sure it's a POST request if request.method == "POST": # sanity check: make sure it's a POST request
print("Request method was POST!") print("Request method was POST!")
os.chmod("uploadkeys", settings.UPLOADKEYS_CHMOD) with open(settings.ENCKEY_PATH,"rb") as enckey: # load encryption key
print("Changed permissions of `uploadkeys`") key = enckey.read()
with open("uploadkeys", "r") as keyfile: # load valid keys f = Fernet(key)
validkeys = keyfile.readlines()
validkeys = [x.strip("\n") for x in validkeys] with open("uploadkeys", "rb") as keyfile:
encrypted_data = keyfile.read()
decrypted_data = str(f.decrypt(encrypted_data).decode('utf-8'))
decrypted_data = decrypted_data.splitlines()
validkeys = [x.strip("\n") for x in decrypted_data]
while "" in validkeys: while "" in validkeys:
validkeys.remove("") validkeys.remove("")
print("removed blank key") print("Removed blank key(s)")
print("Valid keys: {0}".format(validkeys))
print("Loaded validkeys") print("Loaded validkeys")
if "uploadKey" in request.form: # if an uploadKey was provided if "uploadKey" in request.form: # if an uploadKey was provided
if request.form["uploadKey"] in validkeys: # check if uploadKey is valid if request.form["uploadKey"] in validkeys: # check if uploadKey is valid
print("Key is valid!") print("Key is valid!")
if "imageUpload" in request.files: # check if image to upload was provided if "imageUpload" in request.files: # check if image to upload was provided
f = request.files["imageUpload"] # f is the image to upload f = request.files["imageUpload"] # f is the image to upload
print("Found uploaded image")
else: else:
print("No image upload was found!") print("No image upload was found!")
return jsonify({'status': 'error', 'error': 'NO_IMAGE_UPLOADED'}), status.HTTP_400_BAD_REQUEST return jsonify({'status': 'error', 'error': 'NO_IMAGE_UPLOADED'}), status.HTTP_400_BAD_REQUEST
@ -73,23 +63,23 @@ def upload():
return jsonify({'status': 'error', 'error': 'FILENAME_BLANK'}), status.HTTP_400_BAD_REQUEST return jsonify({'status': 'error', 'error': 'FILENAME_BLANK'}), status.HTTP_400_BAD_REQUEST
fext = Path(f.filename).suffix # get the uploaded extension fext = Path(f.filename).suffix # get the uploaded extension
print("Uploaded file extensions is {0}".format(fext))
if allowed_extension(fext): # if the extension is allowed if allowed_extension(fext): # if the extension is allowed
fname = generate_name(fext) # generate file name print("Generating file with extension {0}".format(fext))
fname = functions.generate_name() + fext # generate file name
print("Generated name: {0}".format(fname)) print("Generated name: {0}".format(fname))
if f: # if the uploaded image exists if f: # if the uploaded image exists
print("Uploaded image exists, obviously.") print("Uploaded image exists")
f.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image f.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image
print("Saved to {0}".format(fname)) print("Saved to {0}".format(fname))
url = settings.ROOTURL + fname # construct the url to the image url = settings.ROOTURL + fname # construct the url to the image
if settings.SAVELOG != "/dev/null": if settings.SAVELOG != "/dev/null":
print("Saving to savelog")
log_savelog(request.form["uploadKey"], request.remote_addr, fname) log_savelog(request.form["uploadKey"], request.remote_addr, fname)
print("Logged message to savelog")
print("Returning json response") print("Returning json response")
return jsonify({'status': 'success', 'url': url, 'name': fname, 'uploadedName': f.filename}), status.HTTP_201_CREATED return jsonify({'status': 'success', 'url': url, 'name': fname, 'uploadedName': f.filename}), status.HTTP_201_CREATED
else: # this shouldn't happen else: # this shouldn't happen
print("Um... uploaded image is nonexistent..? Please report this error!") print("Um... uploaded image... is nonexistent? Please report this error!")
return jsonify({'status': 'error', 'error': 'UPLOADED_IMAGE_FAILED_SANITY_CHECK_1'}), status.HTTP_400_BAD_REQUEST return jsonify({'status': 'error', 'error': 'UPLOADED_IMAGE_FAILED_SANITY_CHECK_1'}), status.HTTP_400_BAD_REQUEST
else: # if the extension was invalid else: # if the extension was invalid

103
keygen.py Normal file
View File

@ -0,0 +1,103 @@
from cryptography.fernet import Fernet
from cryptography.fernet import InvalidToken
from pathlib import Path
import settings
import string
import secrets
import sys
import os
# Check if encryption key already exists
enckey = Path(settings.ENCKEY_PATH)
if enckey.is_file():
print("Encryption key found.")
else:
print("Encryption key not found.")
print("Generating key...")
key = Fernet.generate_key()
with open(settings.ENCKEY_PATH, "wb") as key_file:
key_file.write(key)
print("Encryption key generated and stored in secret.key.")
# Load encryption key
def load_key():
with open(settings.ENCKEY_PATH, "rb") as kf:
kdata = kf.read()
return kdata
# Encrypting and storing of key
def encrypt_key(message):
key = load_key()
keyf = Fernet(key)
with open('uploadkeys', 'a+') as uploadkeys:
print(str(token), file=uploadkeys)
with open("uploadkeys", "rb") as keyfile:
keyfile_data = keyfile.read()
encrypted_data = keyf.encrypt(keyfile_data)
with open("uploadkeys", "wb") as keyfile:
keyfile.write(encrypted_data)
def ask_yn(msg):
resps = {"y": True, "n": False}
ask = True
while ask:
proceedraw = input(msg)
if proceedraw.lower() in resps.keys():
proceed = resps[proceedraw]
ask = False
else:
print("Invalid response.")
return proceed
start = ask_yn("Have you run this program as the correct user (for example, nginx uses www-data)? [y/n] ")
if not start:
print("Please run this as the correct user with: sudo su [user] -s /bin/sh -c 'python3 keygen/py'")
else:
N = 64 # Size of token
# Generate key
token = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(N))
# Decrypt the existing keyfile
key = load_key()
keyf = Fernet(key)
genkey = True
uploadkeysp = Path("uploadkeys")
if not uploadkeysp.is_file():
uploadkeysp.touch()
else:
with open("uploadkeys", "rb") as ukf:
# read the encrypted data
encrypted_data = ukf.read()
try:
decrypted_data = keyf.decrypt(encrypted_data) # decrypt data
with open("uploadkeys", "wb") as ukf:
ukf.write(decrypted_data) # write the original file
except InvalidToken:
print("The encrypted key data is invalid and cannot be read.")
print("It may be necessary to clear the file entirely, which will invalidate all tokens.")
proceed = ask_yn("Do you wish to proceed to clearing the uploadkeys file? [y/n] ")
if proceed:
os.remove("uploadkeys")
print("Removed uploadkeys file.")
proceed2 = ask_yn("Would you like to continue and generate a new token? [y/n] ")
if not proceed2:
genkey = False
if genkey:
print("Your new token is: " + str(token)) # Print token
encrypt_key(str(token)) # Encrypt the key and save

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Flask_API==2.0
cryptography==3.1
Flask==1.1.2

View File

@ -1,7 +1,7 @@
UPLOAD_FOLDER = "/var/www/img" UPLOAD_FOLDER = "/path/to/images"
ALLOWED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".svg", ".bmp", ".gif", ".ico", ".webp"] ALLOWED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".svg", ".bmp", ".gif", ".ico", ".webp"]
ROOTURL = "https://img.bbaovanc.com/" ROOTURL = "https://example.com/"
SAVELOG = "savelog.log" SAVELOG = "savelog.log"
SAVELOG_CHMOD = 0o644 SAVELOG_CHMOD = 0o644
UPLOADKEYS_CHMOD = 0o600
SAVELOG_KEYPREFIX = 4 SAVELOG_KEYPREFIX = 4
ENCKEY_PATH = "secret.key"