16 Commits

Author SHA1 Message Date
0b22731076 Remove a few leftover lines 2020-09-03 21:28:14 -05:00
9f2c7c2b88 Clean up and reorganize keygen.py
This makes it easier to add new features
- Clearer variable names
- Split up large functions into smaller ones for each action
- Help make it easier to add new features
2020-09-03 20:20:22 -05:00
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 183 additions and 35 deletions

3
.gitignore vendored
View File

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

View File

@ -1,4 +1,26 @@
# 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/",
"SAVELOG": "savelog.log",
"SAVELOG_CHMOD": "0o644",
"UPLOADKEYS_CHMOD": "0o400",
"SAVELOG_KEYPREFIX": 4,
"ENCKEY_PATH": "secret.key"
}
deftypes = {
@ -19,8 +19,8 @@ deftypes = {
"ROOTURL": str,
"SAVELOG": str,
"SAVELOG_CHMOD": int,
"UPLOADKEYS_CHMOD": int,
"SAVELOG_KEYPREFIX": int,
"ENCKEY_PATH": str,
}
@ -94,6 +94,16 @@ if "ROOTURL" in checksettings:
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
if "SAVELOG" in checksettings:
print("[*] SAVELOG was interpreted to be {0}".format(settings.SAVELOG))
@ -126,6 +136,10 @@ if not uploadfolder_exists:
summarygood = False
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:
summarygood = False
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 cryptography.fernet import Fernet
from flask_api import status
from pathlib import Path
import string
import random
import os
import datetime
import settings # app settings (such as allowed extensions)
ALPHANUMERIC = string.ascii_letters + string.digits # uppercase, lowercase, and numbers
import functions # custom functions
app = Flask(__name__) # app is the app
@ -21,15 +19,6 @@ def allowed_extension(testext):
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):
if settings.SAVELOG_KEYPREFIX > 0:
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))
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
@app.route("/upload", methods = ["POST"])
def upload():
if request.method == "POST": # sanity check: make sure it's a POST request
print("Request method was POST!")
os.chmod("uploadkeys", settings.UPLOADKEYS_CHMOD)
print("Changed permissions of `uploadkeys`")
with open("uploadkeys", "r") as keyfile: # load valid keys
validkeys = keyfile.readlines()
validkeys = [x.strip("\n") for x in validkeys]
with open(settings.ENCKEY_PATH,"rb") as enckey: # load encryption key
key = enckey.read()
f = Fernet(key)
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:
validkeys.remove("")
print("removed blank key")
print("Valid keys: {0}".format(validkeys))
print("Removed blank key(s)")
print("Loaded validkeys")
if "uploadKey" in request.form: # if an uploadKey was provided
if request.form["uploadKey"] in validkeys: # check if uploadKey is valid
print("Key is valid!")
if "imageUpload" in request.files: # check if image to upload was provided
f = request.files["imageUpload"] # f is the image to upload
print("Found uploaded image")
else:
print("No image upload was found!")
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
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
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))
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
print("Saved to {0}".format(fname))
url = settings.ROOTURL + fname # construct the url to the image
if settings.SAVELOG != "/dev/null":
print("Saving to savelog")
log_savelog(request.form["uploadKey"], request.remote_addr, fname)
print("Logged message to savelog")
print("Returning json response")
return jsonify({'status': 'success', 'url': url, 'name': fname, 'uploadedName': f.filename}), status.HTTP_201_CREATED
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
else: # if the extension was invalid

108
keygen.py Normal file
View File

@ -0,0 +1,108 @@
from cryptography.fernet import Fernet
from cryptography.fernet import InvalidToken
from pathlib import Path
import settings
import string
import secrets
import sys
import os
# Load secret
def load_secret():
with open(settings.ENCKEY_PATH, "rb") as sf:
secret = sf.read()
return secret
# Encrypting and storing of key
def append_uploadkey(akey):
with open('uploadkeys', 'a+') as uploadkeysf:
print(str(akey), file=uploadkeysf)
def decrypt_uploadkeys():
with open("uploadkeys", "rb") as uploadkeysf:
uploadkeys_data = uploadkeysf.read()
try:
secret = load_secret()
secretf = Fernet(secret)
decrypted_data = secretf.decrypt(uploadkeys_data) # decrypt data
with open("uploadkeys", "wb") as ukf:
ukf.write(decrypted_data) # write the original file
print("Done decrypting") # debug
return True
except InvalidToken:
print("InvalidToken") # debug
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 key? [y/n] ")
if not proceed2:
return False
else:
return True
else:
return False
def encrypt_uploadkeys():
with open("uploadkeys", "rb") as uploadkeysf:
uploadkeys_data = uploadkeysf.read()
secret = load_secret()
secretf = Fernet(secret)
encrypted_data = secretf.encrypt(uploadkeys_data)
with open("uploadkeys", "wb") as uploadkeysf:
uploadkeysf.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
# Check if encryption secret already exists
if Path(settings.ENCKEY_PATH).is_file():
print("Encryption secret found.")
else:
print("Encryption secret not found.")
print("Generating secret...")
newsecret = Fernet.generate_key()
with open(settings.ENCKEY_PATH, "wb") as secret_file:
secret_file.write(newsecret)
print("Encryption secret generated and stored in {0}".format(settings.ENCKEY_PATH))
if __name__ == "__main__":
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:
uploadkeysp = Path("uploadkeys")
if not uploadkeysp.is_file():
uploadkeysp.touch()
if decrypt_uploadkeys(): # Decrypt the file
N = 64 # Size of key
key = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(N))
print("Your new key is: " + str(key)) # Print key
append_uploadkey(key) # Save the new key to file unencrypted
encrypt_uploadkeys() # Encrypt the uploadkeys file
else:
print("Exiting.")

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"]
ROOTURL = "https://img.bbaovanc.com/"
ROOTURL = "https://example.com/"
SAVELOG = "savelog.log"
SAVELOG_CHMOD = 0o644
UPLOADKEYS_CHMOD = 0o600
SAVELOG_KEYPREFIX = 4
ENCKEY_PATH = "secret.key"