1
0
mirror of https://git.ikl.sh/132ikl/liteshort.git synced 2023-08-10 21:13:04 +03:00

Add PyPi package, add standard config paths

This commit is contained in:
132ikl 2020-04-10 02:09:31 -04:00
parent 8a782c0843
commit 2d0fb48a1e
13 changed files with 194 additions and 130 deletions

3
.gitignore vendored
View File

@ -108,6 +108,3 @@ venv.bak/
# Databases
*.db
# liteshort
config.yml

View File

@ -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

View File

@ -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:

View File

@ -1,6 +0,0 @@
#!/usr/bin/env python3
from liteshort.main import app
if __name__ == "__main__":
app.run()

115
liteshort/config.py Normal file
View File

@ -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

View File

@ -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 <username>: 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'

View File

@ -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

20
liteshort/util.py Normal file
View File

@ -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"))

View File

@ -1,3 +0,0 @@
flask
bcrypt
pyyaml

View File

@ -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 $?

30
setup.py Normal file
View File

@ -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",
)