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

Merge feature/package (#20)

Feature/package
This commit is contained in:
132ikl 2020-04-10 05:05:15 -04:00 committed by GitHub
commit 8e9090bd89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 123 deletions

3
.gitignore vendored
View File

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

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: 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,13 +0,0 @@
[Unit]
Description=uWSGI instance to serve liteshort
After=network.target
[Service]
User=
Group=www-data
WorkingDirectory=/path/to/install
Environment="PATH=/path/to/install/virtualenv/bin"
ExecStart=/path/to/install/virtualenv/bin/uwsgi --ini liteshort.ini
[Install]
WantedBy=multi-user.target

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,9 +5,9 @@ admin_username: 'admin'
# String: Plaintext password to make admin API requests # String: Plaintext password to make admin API requests
# Safe to remove if admin_hashed_password is set # Safe to remove if admin_hashed_password is set
# Default: unset # Default: unset
admin_password: admin_password: CHANGE_ME
# 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 # Please note that authentication takes noticeably longer than using plaintext password
# Don't include the <username>: segment, just the hash # Don't include the <username>: segment, just the hash
# Default: unset (required to start application) # Default: unset (required to start application)
@ -20,7 +20,7 @@ disable_api: false
# String: Secret key used for cookies (used for storage of messages) # String: Secret key used for cookies (used for storage of messages)
# This should be a 12-16 character randomized string with letters, numbers, and symbols # This should be a 12-16 character randomized string with letters, numbers, and symbols
# Default: unset (required to start application) # Default: unset (required to start application)
secret_key: secret_key: CHANGE_ME
# String: Filename of the URL database without extension # String: Filename of the URL database without extension
# Default: 'urls' # Default: 'urls'
@ -56,6 +56,7 @@ subdomain:
# String: URL which takes you to the most recent short URL's destination # String: URL which takes you to the most recent short URL's destination
# Short URLs cannot be created with this string if set # Short URLs cannot be created with this string if set
# Unset to disable
# Default: l # Default: l
latest: 'l' latest: 'l'

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python3
import os import os
import random import random
import sqlite3 import sqlite3
@ -8,84 +7,12 @@ import urllib
import flask import flask
from bcrypt import checkpw from bcrypt import checkpw
from flask import current_app, g, redirect, render_template, request, url_for 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__) 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): def authenticate(username, password):
return username == current_app.config["admin_username"] and check_password( return username == current_app.config["admin_username"] and check_password(
password, current_app.config 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="1.1.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", "appdirs~=1.4.3"],
python_requires=">=3.7",
)

View File

@ -1,10 +1,11 @@
[uwsgi] [uwsgi]
module = liteshort.wsgi:app module = liteshort.wsgi:app
plugin = python3
master = true master = true
processes = 2 processes = 2
socket = liteshort.sock socket = /run/liteshort.sock
chmod-socket = 666 chmod-socket = 666
vacuum = true vacuum = true

9
setup/liteshort.service Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=uWSGI instance to serve liteshort
After=network.target
[Service]
ExecStart=/usr/bin/uwsgi --ini /etc/liteshort/liteshort.ini
[Install]
WantedBy=multi-user.target