Compare commits

...

43 Commits
v0.1 ... master

Author SHA1 Message Date
BBaoVanC 70e84ec8f0
Remove unused flask Response import 2020-12-16 15:25:50 -06:00
BBaoVanC b11c7c2845
Strip EXIF from image before saving 2020-11-24 17:51:37 -06:00
BBaoVanC 86ec88db08
Update .gitignore 2020-11-24 17:37:02 -06:00
BBaoVanC fb416d1c0d
Fix installation instructions 2020-11-20 11:43:35 -06:00
BBaoVanC b20b78fd17
Fix syntax error at imgupload.py:109 2020-11-20 11:38:26 -06:00
BBaoVanC 2b8330e32a
Clean up README.md 2020-10-04 11:15:34 -05:00
BBaoVanC 63818bd371
Change usage of str.format() to f-strings 2020-10-04 11:13:59 -05:00
BBaoVanC 67cb916ac9
Add git checkout step to uWSGI guide 2020-09-26 23:24:59 -05:00
BBaoVanC 9910bc279c
Add imageName field to request a specific name to save to 2020-09-15 17:24:36 -05:00
BBaoVanC f100732e4d
Check if image with same name already exists 2020-09-15 17:20:41 -05:00
BBaoVanC 46420eecda
Remove leftover debug print statements in configtest.py 2020-09-15 16:35:50 -05:00
BBaoVanC 7b372a7b6f
Increase processes in uwsgi.ini.default 2020-09-12 19:48:42 -05:00
BBaoVanC 5570710432
Rewrite installation section in README.md
Also added uWSGI installation tutorial
2020-09-08 00:39:48 -05:00
BBaoVanC ba68674e4e
Add GitHub-specific README.md which links Gitea 2020-09-07 20:31:24 -05:00
BBaoVanC 99ff2c68a3
Change references of gitea.bbaovanc.com to git.bbaovanc.com 2020-09-07 02:07:36 -05:00
BBaoVanC dd069bf395
Add newline after block comments at beginning 2020-09-06 15:24:20 -05:00
BBaoVanC f21adfa04e
Added FAQ and links to sections in README.md 2020-09-06 12:43:44 -05:00
BBaoVanC 3d5c55498f
Clean up and add LICENSE section to README.md 2020-09-06 11:26:13 -05:00
BBaoVanC 5e2be10434
Change references of GitHub to gitea.bbaovanc.com 2020-09-06 11:09:06 -05:00
BBaoVanC 7c1f449bce
Add "verify" field to request to not save image
This makes it easy for the user to debug authentication.
2020-09-05 18:55:56 -05:00
BBaoVanC 0dbcc0e380
Change file extension check to be case-insensitive 2020-09-05 16:21:50 -05:00
BBaoVanC b8b5a2518c
Change abort() calls to JSON responses
This makes the responses more consistent. Now, all responses are JSON.
2020-09-05 15:43:36 -05:00
BBaoVanC 805e545b39
Deduplicate code in keyctl.py and add comments 2020-09-04 19:44:46 -05:00
ItHertzSoGood 9a117817f7 Changed random to secrets for cryptographic security 2020-09-04 14:32:53 -07:00
BBaoVanC 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
BBaoVanC 6f64890e34
Remove unused imports from imgupload.py 2020-09-04 15:24:49 -05:00
BBaoVanC 91db522363
Add keyctl.py for easy management of uploadkeys 2020-09-04 15:19:40 -05:00
BBaoVanC 9d9b93a9ee
Add proper shebangs and block comments 2020-09-04 10:47:20 -05:00
BBaoVanC 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
BBaoVanC 3fcdaa2b10
Fix formatting of README.md 2020-09-02 21:52:13 -05:00
BBaoVanC 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
BBaoVanC 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
BBaoVanC 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
BBaoVanC 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
BBaoVanC 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
dependabot-preview[bot] a587040809
Bump cryptography from 2.8 to 3.1 (#2) 2020-09-01 04:41:50 +00:00
BBaoVanC 8a95dbb0fa
Remove trailing whitespace from lines 2020-08-31 23:09:39 -05:00
BBaoVanC 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
BBaoVanC 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
BBaoVanC 797bebb1a1
Fix ENCKEY_PATH check in configtest.py 2020-08-31 20:29:07 -05:00
quiprr 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
ItHertzSoGood 3d1304b3b0 added encryption to uploadkeys and added a key generator 2020-08-31 17:12:39 -07:00
BBaoVanC 4b624f3fed
Ignore settings.py and add settings.py.default 2020-08-31 18:48:22 -05:00
10 changed files with 412 additions and 62 deletions

7
.github/README.md vendored Normal file
View File

@ -0,0 +1,7 @@
# imgupload
## Moving from GitHub to Gitea
**TL;DR: Please go to my Gitea instance instead of GitHub for anything related to imgupload. [https://git.bbaovanc.com/bbaovanc/imgupload](https://git.bbaovanc.com/bbaovanc/imgupload)**
This repository might not exist on GitHub in the future! Releases will not be released here in the future. Instead, they will be released on the repository on my Gitea instance, which you can find [here](https://git.bbaovanc.com/bbaovanc/imgupload). Issues and pull requests should also be created on Gitea. For now, commits will still be pushed to this repository, but that may change in the future.

29
.gitignore vendored
View File

@ -1,3 +1,8 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -50,6 +55,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
@ -70,6 +76,7 @@ instance/
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
@ -109,6 +116,7 @@ venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
@ -128,7 +136,28 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### VisualStudioCode ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
# imgupload custom
uploadkeys
savelog.log
uwsgi.log
settings.py
functions.py

View File

@ -1,4 +1,88 @@
# imgupload
Python Flask uWSGI application to receive and save images over POST requests.
This project is still in development. Use at your own risk!
<!---![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)-->
## What is imgupload?
imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests.
---
## FAQ
**Where can I send bug reports and feature requests?**
You can create an issue [here](https://git.bbaovanc.com/bbaovanc/imgupload/issues).
**How do I use this program?**
See [Installation](#installation)
**I want to make a pull request. Where should I do that?**
First, fork [this repository](https://git.bbaovanc.com/bbaovanc/imgupload). If you don't have an account on my Gitea site yet, you can either create one, or sign in using your GitHub account. Commit your changes to your fork, and then create a pull request.
---
## Installation
### Using uWSGI
Note: replace `www-data` with whatever user your webserver runs as.
1. Go to /srv: `cd /srv`
2. Clone the repository: `git clone https://git.bbaovanc.com/bbaovanc/imgupload.git`
3. Change ownership of /srv/imgupload: `sudo chown www-data:www-data /srv/imgupload`
4. Enter www-data user: `sudo su www-data`
5. Change directories to /srv/imgupload: `cd /srv/imgupload`
6. Checkout the version you want (replace [version] with desired version tag: `git checkout [version]`
7. Enter the imgupload directory: `cd imgupload`
8. Create a virtualenv: `python3 -m venv env`
9. Enter the virtualenv: `source env/bin/activate`
10. Install dependencies: `python3 -m pip install -r requirements.txt`
11. Leave the www-data user: `exit`
12. Copy the default uWSGI configuration: `sudo cp /srv/imgupload/uwsgi.ini.default /etc/uwsgi/apps-available/imgupload.ini`
13. Modify `/etc/uwsgi/apps-available/imgupload.ini` to your preferences
14. Enable imgupload: `sudo ln -s /etc/uwsgi/apps-available/imgupload.ini /etc/uwsgi/apps-enabled/`
15. Restart uWSGI: `sudo systemctl restart uwsgi`
16. Set up your webserver to proxy the uwsgi.sock
Example NGINX location block:
```nginx
location /upload {
include uwsgi_params;
uwsgi_pass unix:/srv/imgupload/uwsgi.sock;
client_max_body_size 25M;
}
```
### Using Flask development server
#### Setup
```shell
git clone https://git.bbaovanc.com/bbaovanc/imgupload.git
cd imgupload
python3 -m venv env
source env/bin/activate
pip3 install -r requirements.txt
```
#### Run
```shell
export FLASK_APP=imgupload.py
flask run
```
---
## License
_imgupload_ is licensed under the GPLv3 license. For more information, please refer to [`LICENSE`](https://git.bbaovanc.com/bbaovanc/imgupload/src/branch/master/LICENSE)

View File

@ -1,3 +1,10 @@
#!/usr/bin/env python3
"""
configtest.py
Tests the validity of your configuration in settings.py.
"""
import os
import settings as settings
@ -9,7 +16,6 @@ defaults = {
"ROOTURL": "https://img.bbaovanc.com/",
"SAVELOG": "savelog.log",
"SAVELOG_CHMOD": "0o644",
"UPLOADKEYS_CHMOD": "0o400",
"SAVELOG_KEYPREFIX": 4,
}
@ -19,7 +25,6 @@ deftypes = {
"ROOTURL": str,
"SAVELOG": str,
"SAVELOG_CHMOD": int,
"UPLOADKEYS_CHMOD": int,
"SAVELOG_KEYPREFIX": int,
}
@ -30,7 +35,7 @@ unset_settings = [i for i in defaults.keys() if i not in dir(settings)]
if len(unset_settings) > 0:
for unset in unset_settings:
checksettings.remove(unset)
print("[!] {0} is unset. The default value is type {1} with value {2}".format(unset, deftypes[unset].__name__, defaults[unset]))
print(f"[!] {unset} is unset. The default value is type {deftypes[unset].__name__} with value {defaults[unset]}")
else:
print("[" + u"\u2713" + "] Found all required settings!")
@ -40,7 +45,7 @@ typesgood = True
typeswrong = []
for testtype in checksettings:
if type(getattr(settings, testtype)) is not deftypes[testtype]:
print("[!] {0} requires {1}, but is {2}".format(testtype, deftypes[testtype].__name__, type(getattr(settings, testtype)).__name__))
print(f"[!] {testtype} requires {deftypes[testtype].__name__}, but is {type(getattr(settings, testtype)).__name__}")
typeswrong.append(testtype)
typesgood = False
@ -58,7 +63,7 @@ if "ALLOWED_EXTENSIONS" in checksettings:
if len(invalid_exts) > 0:
print("[!] The following extensions listed in ALLOWED_EXTENSIONS are invalid:")
for e in invalid_exts:
print(" {0} is listed in ALLOWED_EXTENSIONS, but doesn't start with a .".format(e))
print(f" {e} is listed in ALLOWED_EXTENSIONS, but doesn't start with a .")
else:
print("[" + u"\u2713" + "] ALLOWED_EXTENSIONS is good!")
@ -68,7 +73,7 @@ uploadfolder_exists = True
if "UPLOAD_FOLDER" in checksettings:
if not os.path.isdir(settings.UPLOAD_FOLDER):
uploadfolder_exists = False
print("[!] The directory set in UPLOAD_FOLDER ('{0}') doesn't exist!".format(settings.UPLOAD_FOLDER))
print(f"[!] The directory set in UPLOAD_FOLDER ('{settings.UPLOAD_FOLDER}') doesn't exist!")
else:
print("[" + u"\u2713" + "] UPLOAD_FOLDER exists!")
@ -80,8 +85,6 @@ if "ROOTURL" in checksettings:
pass
else:
rooturl_good = False
print(settings.ROOTURL)
print(settings.ROOTURL.startswith("https://"))
print("[!] ROOTURL does not start with `http://` or `https://`! This may cause issues!")
if not settings.ROOTURL.endswith("/"):
rooturl_good = False
@ -89,14 +92,14 @@ if "ROOTURL" in checksettings:
if not rooturl_good:
print(" With your current settings, this is what a generated url would look like:")
print(" {0}example.png".format(settings.ROOTURL))
print(f" {settings.ROOTURL}example.png")
else:
print("[" + u"\u2713" + "] ROOTURL is good!")
# Ask the user if SAVELOG is the intended filename
if "SAVELOG" in checksettings:
print("[*] SAVELOG was interpreted to be {0}".format(settings.SAVELOG))
print(f"[*] SAVELOG was interpreted to be {settings.SAVELOG}")
print("[*] If this is not the intended filename, please fix it.")
@ -108,32 +111,32 @@ if len(unset_settings) > 0:
summarygood = False
print("Unset settings:")
for unset in unset_settings:
print(" {0}".format(unset))
print(f" {unset}")
if len(typeswrong) > 0:
summarygood = False
print("Incorrect types:")
for wtype in typeswrong:
print(" {0}".format(wtype))
print(f" {wtype}")
if len(invalid_exts) > 0:
summarygood = False
print("Invalid extensions:")
for wext in invalid_exts:
print(" '{0}'".format(wext))
print(f" '{wext}'")
if not uploadfolder_exists:
summarygood = False
print("UPLOAD_FOLDER ({0}) does not exist!".format(settings.UPLOAD_FOLDER))
print(f"UPLOAD_FOLDER ({settings.UPLOAD_FOLDER}) does not exist!")
if not rooturl_good:
summarygood = False
print("ROOTURL may cause issues!")
print("With current settings, this is what a generated URL would look like:")
print("{0}example.png".format(settings.ROOTURL))
print(f"{settings.ROOTURL}example.png")
if "SAVELOG" in checksettings:
print("[*] SAVELOG is {0}".format(settings.SAVELOG))
print(f"[*] SAVELOG is {settings.SAVELOG}")
if summarygood:
print("[" + u"\u2713" + "] This configuration passes all tests!")

15
functions.py.default Normal file
View File

@ -0,0 +1,15 @@
#!/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,69 +1,63 @@
from flask import Flask, request, jsonify, abort, Response
#!/usr/bin/env python3
"""
imgupload.py
Flask application for processing images uploaded through POST requests.
"""
from flask import Flask, request, jsonify
from flask_api import status
from pathlib import Path
import string
import random
import os
import datetime
from PIL import Image
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
def allowed_extension(testext):
if testext in settings.ALLOWED_EXTENSIONS:
if testext.lower() in settings.ALLOWED_EXTENSIONS:
return True
else:
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:
slogf.write("[{0}] {1}: {2} - {3}\n".format(datetime.datetime.now(), key[:settings.SAVELOG_KEYPREFIX], ip, savedname))
slogf.write(f"[{datetime.datetime.now()}] {key[:settings.SAVELOG_KEYPREFIX]}: {ip} - {savedname}\n")
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
else:
with open(settings.SAVELOG, "a+") as slogf:
slogf.write("[{0}] {1} - {2}\n".format(datetime.datetime.now(), ip, savedname))
slogf.write(f"[{datetime.datetime.now()}] {ip} - {savedname}\n")
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]
while "" in validkeys:
validkeys.remove("")
print("removed blank key")
print("Valid keys: {0}".format(validkeys))
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 "verify" in request.form.keys():
if request.form["verify"] == "true":
print("Request is asking if key is valid (it is)")
return jsonify({'status': 'key_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,42 +67,59 @@ 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("Generated name: {0}".format(fname))
if not "imageName" in request.form.keys():
print(f"Generating file with extension {fext}")
fname = functions.generate_name() + fext # generate file name
print(f"Generated name: {fname}")
else:
fname = request.form["imageName"]
if len(fname) > 0:
print(f"Request imageName: {fname}")
if not fname.lower().endswith(fext.lower()): # if requested name doesn't have the correct extension
fname += fext # add the extension
print(f"Added extension; new filename: {fname}")
else:
print("Requested filename is blank!")
fname = functions.generate_name() + fext # generate a valid filename
print(f"Generated name: {fname}")
if f: # if the uploaded image exists
print("Uploaded image exists, obviously.")
f.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image
print("Saved to {0}".format(fname))
print("Uploaded image exists")
if Path(os.path.join(settings.UPLOAD_FOLDER, fname)).is_file():
print("Requested filename already exists!")
return jsonify({'status': 'error', 'error': 'FILENAME_TAKEN'}), status.HTTP_409_CONFLICT
f.save(f"/tmp/{fname}") # save the image temporarily (before removing EXIF)
image = Image.open(f"/tmp/{fname}")
data = list(image.getdata())
stripped = Image.new(image.mode, image.size)
stripped.putdata(data)
stripped.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image without EXIF
print(f"Saved to {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
print("Uploaded extension is invalid!")
abort(415)
return jsonify({'status': 'error', 'error': 'INVALID_EXTENSION'}), status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
else: # if the key was not valid
print("Key is invalid!")
print("Request key: {0}".format(request.form["uploadKey"]))
abort(401)
print(f"Request key: {request.form['uploadKey']}")
return jsonify({'status': 'error', 'error': 'UNAUTHORIZED'}), status.HTTP_401_UNAUTHORIZED
else: # if uploadKey was not found in request body
print("No uploadKey found in request!")
abort(401)
else: # if the request method wasn't post
print("Request method was not POST!")
abort(405)
return jsonify({'status': 'error', 'error': 'UNAUTHORIZED'}), status.HTTP_401_UNAUTHORIZED
if __name__ == "__main__":
print("Run with `flask` or a WSGI server!")

180
keyctl.py Normal file
View File

@ -0,0 +1,180 @@
#!/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(f"Saved a key to uploadkeys: {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(f" [{i+1}] {showkey}")
def cmd_generate(args):
k = genkey(args.length)
logging.debug(f"Generated a new key: {k}")
savekey(k)
print(f"Your new key is: {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")
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(f"Removed duplicate key: {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(f"Key: {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()

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Flask_API==2.0
Flask==1.1.2
Pillow==8.0.1

View File

@ -1,7 +1,13 @@
UPLOAD_FOLDER = "/var/www/img"
#!/usr/bin/env python3
"""
settings.py
User-defined settings used by imgupload.py.
"""
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

12
uwsgi.ini.default Normal file
View File

@ -0,0 +1,12 @@
[uwsgi]
socket = /srv/imgupload/uwsgi.sock
chmod-socket = 755
chdir = /srv/imgupload
venv = /srv/imgupload/env
master = true
module = imgupload:app
processes = 10
threads = 1
uid = www-data
gid = www-data
plugins = python3,logfile