mirror of
https://git.ikl.sh/132ikl/liteshort.git
synced 2023-08-10 21:13:04 +03:00
commit
8e9090bd89
3
.gitignore
vendored
3
.gitignore
vendored
@ -108,6 +108,3 @@ venv.bak/
|
||||
|
||||
# Databases
|
||||
*.db
|
||||
|
||||
# liteshort
|
||||
config.yml
|
||||
|
2
LICENSE
2
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:
|
||||
|
||||
|
@ -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
115
liteshort/config.py
Normal 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
|
@ -5,9 +5,9 @@ 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: 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
|
||||
# Don't include the <username>: segment, just the hash
|
||||
# Default: unset (required to start application)
|
||||
@ -20,7 +20,7 @@ disable_api: false
|
||||
# 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
|
||||
# Default: unset (required to start application)
|
||||
secret_key:
|
||||
secret_key: CHANGE_ME
|
||||
|
||||
# String: Filename of the URL database without extension
|
||||
# Default: 'urls'
|
||||
@ -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'
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import random
|
||||
import sqlite3
|
||||
@ -8,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
20
liteshort/util.py
Normal 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"))
|
@ -1,3 +0,0 @@
|
||||
flask
|
||||
bcrypt
|
||||
pyyaml
|
@ -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
30
setup.py
Normal 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",
|
||||
)
|
@ -1,10 +1,11 @@
|
||||
[uwsgi]
|
||||
module = liteshort.wsgi:app
|
||||
plugin = python3
|
||||
|
||||
master = true
|
||||
processes = 2
|
||||
|
||||
socket = liteshort.sock
|
||||
socket = /run/liteshort.sock
|
||||
chmod-socket = 666
|
||||
vacuum = true
|
||||
|
9
setup/liteshort.service
Normal file
9
setup/liteshort.service
Normal 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
|
Loading…
Reference in New Issue
Block a user