From 2d0fb48a1e1b55a5a04c9aa20963ed740cb7df76 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Fri, 10 Apr 2020 02:09:31 -0400 Subject: [PATCH] Add PyPi package, add standard config paths --- .gitignore | 3 - CONVENTIONS | 38 +++--- LICENSE | 2 +- liteshort.py | 6 - liteshort/config.py | 115 +++++++++++++++++++ config.yml => liteshort/config.template.yml | 7 +- liteshort/main.py | 76 +----------- liteshort/util.py | 20 ++++ requirements.txt | 3 - securepass.sh | 24 ---- setup.py | 30 +++++ liteshort.ini => setup/liteshort.ini | 0 liteshort.service => setup/liteshort.service | 0 13 files changed, 194 insertions(+), 130 deletions(-) delete mode 100755 liteshort.py create mode 100644 liteshort/config.py rename config.yml => liteshort/config.template.yml (95%) create mode 100644 liteshort/util.py delete mode 100644 requirements.txt delete mode 100755 securepass.sh create mode 100644 setup.py rename liteshort.ini => setup/liteshort.ini (100%) rename liteshort.service => setup/liteshort.service (100%) diff --git a/.gitignore b/.gitignore index f4a3e19..b783f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,3 @@ venv.bak/ # Databases *.db - -# liteshort -config.yml diff --git a/CONVENTIONS b/CONVENTIONS index 7152125..3d1f3c8 100644 --- a/CONVENTIONS +++ b/CONVENTIONS @@ -1,3 +1,5 @@ +TODO: Move to wiki + The following is a description of design philosphies and standards for liteshort. This document is mostly for developers, however if you are interested feel free to take a peek. DESIGN PHILOSPHIES: @@ -7,21 +9,25 @@ DESIGN PHILOSPHIES: - Liteshort should be simple to install and maintain. Installing should be a simple process, whether from a package manager or when installing from source. - Liteshort should follow system design standards. This includes basic Unix design principles and the Filesystem Hierarchy Standard. -FILE STRUCTURE: - - Executable file: /usr/bin/liteshort - - Source directory: /usr/lib/python3/dist-packages/liteshort - - Config directory: /etc/liteshort/ - - Socket file: /run/liteshort.sock -PYPI PACKAGE: -The PyPi package should provide no more than the following: - - Install source to Python package directory - - Install template config files - - Install debug executable (liteshort.py) installed - - Install python-only requirements (no uwsgi) +PROPOSAL FOR A DEBIAN PACKAGE -DEBIAN PACAKGE: -The Debian package should provide no more than the following: - - Install uwsgi - - Install and enable systemd service - - Should be based upon pybuild, as to prevent redundant packaging +File Structure: + x Debug Executable file: liteshort in $PATH + x Hashpw executable file: lshash in $PATH + x Source directory: system Python module folder (eg. /usr/lib/python3/dist-packages/liteshort) + x Config directory: /etc/liteshort/ OR appdirs.site_config_dir OR appdirs.user_config_dir + - Socket file (if applicable): /run/liteshort.sock + +Pypi Package: + The PyPi package should provide no more than the following: + x Install source to Python package directory + x Install template config files + x Install debug executables installed + x Install minimum requirements (no uwsgi) + +Debian Pacakge: + The Debian package should provide no more than the following: + - Install uwsgi + - Install and enable systemd service + - Should be based upon pybuild, as to prevent redundant packaging diff --git a/LICENSE b/LICENSE index 6db2ded..410d83e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 132ikl +Copyright 2020 Steven Spangler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/liteshort.py b/liteshort.py deleted file mode 100755 index 1ef84b6..0000000 --- a/liteshort.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -from liteshort.main import app - -if __name__ == "__main__": - app.run() diff --git a/liteshort/config.py b/liteshort/config.py new file mode 100644 index 0000000..beed2be --- /dev/null +++ b/liteshort/config.py @@ -0,0 +1,115 @@ +import logging +from pathlib import Path +from shutil import copyfile + +from appdirs import site_config_dir, user_config_dir +from pkg_resources import resource_filename +from yaml import safe_load + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) + + +def get_config(): + APP = "liteshort" + AUTHOR = "132ikl" + + paths = [ + Path("/etc/liteshort"), + Path(site_config_dir(APP, AUTHOR)), + Path(user_config_dir(APP, AUTHOR)), + Path(), + ] + + for path in paths: + f = path / "config.yml" + if f.exists(): + LOGGER.debug(f"Selecting config file {f}") + return open(f, "r") + + for path in paths: + try: + path.mkdir(exist_ok=True) + template = resource_filename(__name__, "config.template.yml") + copyfile(template, (path / "config.template.yml")) + copyfile(template, (path / "config.yml")) + return open(path / "config.yml", "r") + except (PermissionError, OSError) as e: + LOGGER.warn(f"Failed to create config in {path}") + LOGGER.debug("", exc_info=True) + + raise FileNotFoundError("Cannot find config.yml, and failed to create it") + + +# TODO: yikes +def load_config(): + with get_config() as config: + configYaml = safe_load(config) + config = { + k.lower(): v for k, v in configYaml.items() + } # Make config keys case insensitive + + req_options = { + "admin_username": "admin", + "database_name": "urls", + "random_length": 4, + "allowed_chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", + "random_gen_timeout": 5, + "site_name": "liteshort", + "site_domain": None, + "show_github_link": True, + "secret_key": None, + "disable_api": False, + "subdomain": "", + "latest": "l", + "selflinks": False, + "blocklist": [], + } + + config_types = { + "admin_username": str, + "database_name": str, + "random_length": int, + "allowed_chars": str, + "random_gen_timeout": int, + "site_name": str, + "site_domain": (str, type(None)), + "show_github_link": bool, + "secret_key": str, + "disable_api": bool, + "subdomain": (str, type(None)), + "latest": (str, type(None)), + "selflinks": bool, + "blocklist": list, + } + + for option in req_options.keys(): + if ( + option not in config.keys() + ): # Make sure everything in req_options is set in config + config[option] = req_options[option] + + for option in config.keys(): + if option in config_types: + matches = False + if type(config_types[option]) is not tuple: + config_types[option] = ( + config_types[option], + ) # Automatically creates tuple for non-tuple types + for req_type in config_types[ + option + ]: # Iterates through tuple to allow multiple types for config options + if type(config[option]) is req_type: + matches = True + if not matches: + raise TypeError(option + " is incorrect type") + if not config["disable_api"]: + if "admin_hashed_password" in config.keys() and config["admin_hashed_password"]: + config["password_hashed"] = True + elif "admin_password" in config.keys() and config["admin_password"]: + config["password_hashed"] = False + else: + raise TypeError( + "admin_password or admin_hashed_password must be set in config.yml" + ) + return config diff --git a/config.yml b/liteshort/config.template.yml similarity index 95% rename from config.yml rename to liteshort/config.template.yml index f935132..5f06b36 100644 --- a/config.yml +++ b/liteshort/config.template.yml @@ -5,13 +5,13 @@ admin_username: 'admin' # String: Plaintext password to make admin API requests # Safe to remove if admin_hashed_password is set # Default: unset -admin_password: +#admin_password: -# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use securepass.sh to generate +# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use lshash to generate # Please note that authentication takes noticeably longer than using plaintext password # Don't include the : segment, just the hash # Default: unset (required to start application) -#admin_hashed_password: +admin_hashed_password: # Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set. # Default: false @@ -56,6 +56,7 @@ subdomain: # String: URL which takes you to the most recent short URL's destination # Short URLs cannot be created with this string if set +# Unset to disable # Default: l latest: 'l' diff --git a/liteshort/main.py b/liteshort/main.py index f32044f..976a21a 100644 --- a/liteshort/main.py +++ b/liteshort/main.py @@ -7,84 +7,12 @@ import urllib import flask from bcrypt import checkpw from flask import current_app, g, redirect, render_template, request, url_for -from yaml import safe_load + +from .config import load_config app = flask.Flask(__name__) -def load_config(): - with open("config.yml") as config: - configYaml = safe_load(config) - config = { - k.lower(): v for k, v in configYaml.items() - } # Make config keys case insensitive - - req_options = { - "admin_username": "admin", - "database_name": "urls", - "random_length": 4, - "allowed_chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", - "random_gen_timeout": 5, - "site_name": "liteshort", - "site_domain": None, - "show_github_link": True, - "secret_key": None, - "disable_api": False, - "subdomain": "", - "latest": "l", - "selflinks": False, - "blocklist": [], - } - - config_types = { - "admin_username": str, - "database_name": str, - "random_length": int, - "allowed_chars": str, - "random_gen_timeout": int, - "site_name": str, - "site_domain": (str, type(None)), - "show_github_link": bool, - "secret_key": str, - "disable_api": bool, - "subdomain": (str, type(None)), - "latest": (str, type(None)), - "selflinks": bool, - "blocklist": list, - } - - for option in req_options.keys(): - if ( - option not in config.keys() - ): # Make sure everything in req_options is set in config - config[option] = req_options[option] - - for option in config.keys(): - if option in config_types: - matches = False - if type(config_types[option]) is not tuple: - config_types[option] = ( - config_types[option], - ) # Automatically creates tuple for non-tuple types - for req_type in config_types[ - option - ]: # Iterates through tuple to allow multiple types for config options - if type(config[option]) is req_type: - matches = True - if not matches: - raise TypeError(option + " is incorrect type") - if not config["disable_api"]: - if "admin_hashed_password" in config.keys() and config["admin_hashed_password"]: - config["password_hashed"] = True - elif "admin_password" in config.keys() and config["admin_password"]: - config["password_hashed"] = False - else: - raise TypeError( - "admin_password or admin_hashed_password must be set in config.yml" - ) - return config - - def authenticate(username, password): return username == current_app.config["admin_username"] and check_password( password, current_app.config diff --git a/liteshort/util.py b/liteshort/util.py new file mode 100644 index 0000000..29a383e --- /dev/null +++ b/liteshort/util.py @@ -0,0 +1,20 @@ +from getpass import getpass + +import bcrypt + + +def hash_passwd(): + salt = bcrypt.gensalt() + try: + unhashed = getpass("Type password to hash: ") + unhashed2 = getpass("Confirm: ") + except (KeyboardInterrupt, EOFError): + pass + + if unhashed != unhashed2: + print("Passwords don't match.") + return None + + hashed = bcrypt.hashpw(unhashed.encode("utf-8"), salt) + + print("Password hash: " + hashed.decode("utf-8")) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8985154..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask -bcrypt -pyyaml diff --git a/securepass.sh b/securepass.sh deleted file mode 100755 index 0d1426b..0000000 --- a/securepass.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -## bcrypt passwd generator ## -############################# -CMD=$(which htpasswd 2>/dev/null) -OPTS="-nBC 12" - -read -p "Username: " USERNAME - -check_config() { - if [ -z $CMD ]; then - printf "Exiting: htpasswd is missing.\n" - exit 1 - fi - - if [ -z "$USERNAME" ]; then - usage - fi -} - -check_config $USERNAME -printf "Generating Bcrypt hash for username: $USERNAME\n\n" -$CMD $OPTS $USERNAME -exit $? diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..57bb6fe --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="liteshort", + version="2.0.0", + author="Steven Spangler", + author_email="132@ikl.sh", + description="User-friendly, actually lightweight, and configurable URL shortener", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/132ikl/liteshort", + packages=setuptools.find_packages(), + package_data={"liteshort": ["templates/*", "static/*", "config.template.yml"]}, + entry_points={ + "console_scripts": [ + "liteshort = liteshort.main:app.run", + "lshash = liteshort.util:hash_passwd", + ] + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + ], + install_requires=["flask~=1.1.2", "bcrypt~=3.1.7", "pyyaml~=5.3.1"], + python_requires=">=3.7", +) diff --git a/liteshort.ini b/setup/liteshort.ini similarity index 100% rename from liteshort.ini rename to setup/liteshort.ini diff --git a/liteshort.service b/setup/liteshort.service similarity index 100% rename from liteshort.service rename to setup/liteshort.service