12 Commits
v1.0 ... v2.1.2

Author SHA1 Message Date
805e545b39 Deduplicate code in keyctl.py and add comments 2020-09-04 19:44:46 -05:00
9a117817f7 Changed random to secrets for cryptographic security 2020-09-04 14:32:53 -07:00
c9cd6469a9 Change a couple print statements to logging.info
Quiet mode is now a little more useful!
2020-09-04 15:37:34 -05:00
6f64890e34 Remove unused imports from imgupload.py 2020-09-04 15:24:49 -05:00
91db522363 Add keyctl.py for easy management of uploadkeys 2020-09-04 15:19:40 -05:00
9d9b93a9ee Add proper shebangs and block comments 2020-09-04 10:47:20 -05:00
565a91e4ec Remove uploadkeys encryption features
It doesn't really make sense to encrypt the keys, but store the secret
literally in the same directory. uploadkeys will now be stored in
plaintext. The branch `legacy` has the old code from before this commit.
2020-09-03 21:44:58 -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
9 changed files with 241 additions and 151 deletions

2
.gitignore vendored
View File

@ -133,4 +133,4 @@ uploadkeys
savelog.log savelog.log
uwsgi.log uwsgi.log
settings.py settings.py
secret.key functions.py

View File

@ -4,6 +4,23 @@
### What is imgupload? ### What is imgupload?
imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests. imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests.
### Usage ### Installation
Make sure you install the dependencies first. To do this, run `sudo python3 -m pip install -r requirements.txt`. 1. Clone the repository: `git clone https://github.com/BBaoVanC/imgupload.git`
To deploy imgupload, run `flask run`. 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

@ -1,3 +1,9 @@
#!/usr/bin/env python3
"""
configtest.py
Tests the validity of your configuration in settings.py.
"""
import os import os
import settings as settings import settings as settings
@ -10,7 +16,6 @@ defaults = {
"SAVELOG": "savelog.log", "SAVELOG": "savelog.log",
"SAVELOG_CHMOD": "0o644", "SAVELOG_CHMOD": "0o644",
"SAVELOG_KEYPREFIX": 4, "SAVELOG_KEYPREFIX": 4,
"ENCKEY_PATH": "secret.key"
} }
deftypes = { deftypes = {
@ -20,7 +25,6 @@ deftypes = {
"SAVELOG": str, "SAVELOG": str,
"SAVELOG_CHMOD": int, "SAVELOG_CHMOD": int,
"SAVELOG_KEYPREFIX": int, "SAVELOG_KEYPREFIX": int,
"ENCKEY_PATH": str,
} }
@ -94,16 +98,6 @@ 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))
@ -136,10 +130,6 @@ 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!")

14
functions.py.default Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""
functions.py
Functions used by imgupload which can be easily customized.
"""
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,16 +1,17 @@
#!/usr/bin/env python3
"""
imgupload.py
Flask application for processing images uploaded through POST requests.
"""
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 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
@ -22,15 +23,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:
@ -46,20 +38,13 @@ 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!")
with open(settings.ENCKEY_PATH,"rb") as enckey: # load encryption key with open("uploadkeys", "r") as keyfile: # load valid keys
key = enckey.read() validkeys = keyfile.readlines()
f = Fernet(key) 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(s)")
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!")
@ -77,7 +62,7 @@ def upload():
fext = Path(f.filename).suffix # get the uploaded extension fext = Path(f.filename).suffix # get the uploaded extension
if allowed_extension(fext): # if the extension is allowed if allowed_extension(fext): # if the extension is allowed
print("Generating file with extension {0}".format(fext)) print("Generating file with extension {0}".format(fext))
fname = generate_name(fext) # generate file name 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

181
keyctl.py Normal file
View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
keyctl.py
Command-line utility for easy management of the uploadkeys file.
"""
from pathlib import Path
import argparse
import logging
import secrets
import string
def read_keyfile():
with open("uploadkeys", "r") as keyfile: # open uploadkeys
keys = keyfile.readlines() # read all the keys
logging.debug("Read uploadkeys")
keys = [x.strip("\n") for x in keys] # strip newlines from keys
logging.debug("Stripped newlines from keys")
return keys
def genkey(length):
key = ''.join(secrets.choice(string.ascii_letters + string.digits) for x in range(length))
return key
def savekey(key):
if not Path("uploadkeys").is_file(): # if uploadkeys doesn't exist, log an info message
logging.info("uploadkeys file doesn't exist, it will be created.")
with open("uploadkeys", "a+") as keyfile:
keyfile.write(str(key) + "\n") # add the key
logging.debug("Saved a key to uploadkeys: {0}".format(key))
def rmkey(delkey):
removedkey = False
allkeys = read_keyfile()
if delkey in allkeys: # if the key to remove exists
allkeys.remove(delkey) # remove the first instance of the key
removedkey = True
logging.debug("Removed one instance of the key")
with open("uploadkeys", "w") as keyfile:
for k in allkeys:
keyfile.write(k + "\n") # write the remaining keys
if removedkey:
return True
else:
return False
def find_duplicates():
allkeys = read_keyfile()
seen = set()
ukeys = []
dupkeys = []
for x in allkeys:
if x not in seen:
ukeys.append(x)
seen.add(x)
else:
dupkeys.append(x)
return dupkeys
def get_keys():
validkeys = read_keyfile()
while "" in validkeys:
validkeys.remove("")
logging.debug("Removed blank keys")
return validkeys
def cmd_list(args):
validkeys = get_keys()
print("List of upload keys:")
for i in range(len(validkeys)):
showkey = validkeys[i][:6]
if len(validkeys[i]) > 6:
showkey += "..." # add ellipses since the key was shortened in list
print(" [{0}] {1}".format(i+1, showkey))
def cmd_generate(args):
k = genkey(args.length)
logging.debug("Generated a new key: {0}".format(k))
savekey(k)
print("Your new key is: {0}".format(k))
def cmd_add(args):
print("Please type/paste the key you would like to add.")
akr = input("> ")
ak = akr.strip()
print()
logging.debug("Ran strip() on key")
print(ak)
if input("Is the above key correct? [y/N] ").lower() == "y":
logging.debug("Interpreted as yes")
ask_for_key = False
savekey(ak)
logging.info("Added.")
else:
logging.debug("Interpreted as no")
print("No key has been saved.")
def cmd_remove(args):
if rmkey(args.key):
logging.debug("Successfully removed the requested key")
else:
logging.info("No key was removed.")
def cmd_dedupe(args):
dupes = find_duplicates()
if len(dupes) > 0:
for d in dupes:
r = rmkey(d)
logging.debug(r)
logging.info("Removed duplicate key: {0}".format(d))
else:
logging.info("[" + u"\u2713" + "] No duplicate keys found!")
def cmd_show(args):
for k in get_keys():
if k[:6] == args.prefix:
print("Key: {0}".format(k))
break
parser = argparse.ArgumentParser() # create instance of argument parser class
parlog = parser.add_mutually_exclusive_group()
parlog.add_argument("-v", "--verbose", help="show debugging messages", action="store_true")
parlog.add_argument("-q", "--quiet", help="show only warning messages and up", action="store_true")
subparsers = parser.add_subparsers(help="sub-commands")
parser_list = subparsers.add_parser("list", help="list the beginning of each key")
parser_list.set_defaults(func=cmd_list)
parser_gen = subparsers.add_parser("generate", help="generate a key and save it to uploadkeys")
parser_gen.add_argument("length", help="length of key to generate", default=64, type=int, nargs="?")
parser_gen.set_defaults(func=cmd_generate)
parser_add = subparsers.add_parser("add", help="prompts for a key to add to uploadkeys")
parser_add.set_defaults(func=cmd_add)
parser_remove = subparsers.add_parser("remove", help="remove (one instance of) a key from uploadkeys")
parser_remove.add_argument("key", help="key to remove")
parser_remove.set_defaults(func=cmd_remove)
parser_dedupe = subparsers.add_parser("dedupe", help="remove duplicate keys")
parser_dedupe.set_defaults(func=cmd_dedupe)
parser_show = subparsers.add_parser("show", help="show the full key based on the first 6 characters")
parser_show.add_argument("prefix", help="first 6 characters of key (shown by `python3 keyctl.py list`)")
parser_show.set_defaults(func=cmd_show)
args = parser.parse_args() # parse the arguments
if args.verbose:
loglevel = logging.DEBUG
elif args.quiet:
loglevel = logging.WARNING
else:
loglevel = logging.INFO
logging.basicConfig(level=loglevel, format="%(levelname)s: %(message)s")
try:
args.func(args)
except AttributeError:
logging.error("AttributeError")
parser.print_help()

102
keygen.py
View File

@ -1,102 +0,0 @@
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 the script was run as root
if os.geteuid() != 0:
exit("Root privileges are necessary to run this script.\nPlease try again as root or using `sudo`.")
# 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
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

View File

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

View File

@ -1,7 +1,13 @@
#!/usr/bin/env python3
"""
settings.py
User-defined settings used by imgupload.py.
"""
UPLOAD_FOLDER = "/path/to/images" 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://example.com/" ROOTURL = "https://example.com/"
SAVELOG = "savelog.log" SAVELOG = "savelog.log"
SAVELOG_CHMOD = 0o644 SAVELOG_CHMOD = 0o644
SAVELOG_KEYPREFIX = 4 SAVELOG_KEYPREFIX = 4
ENCKEY_PATH = "secret.key"