diff --git a/main.py b/main.py index 01b2388..ed5c864 100644 --- a/main.py +++ b/main.py @@ -1,20 +1,18 @@ """ Main Flask-based app for Slinky """ + from flask import Flask, Response, render_template -from flask_bootstrap import Bootstrap # type: ignore[import] from werkzeug.middleware.proxy_fix import ProxyFix from slinky.web import protect, slinky_webapp app = Flask(__name__) -app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) # type: ignore[assignment] +app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) app.register_blueprint(slinky_webapp) -Bootstrap(app) - -@app.route('/') +@app.route("/") @protect def index() -> Response: """ @@ -23,4 +21,4 @@ def index() -> Response: Returns: str: string of page content """ - return Response(render_template('index.html'), 200) + return Response(render_template("index.html"), 200) diff --git a/requirements.txt b/requirements.txt index 555122e..50083b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ flask -flask_bootstrap flask_wtf psycopg2-binary pyyaml diff --git a/slinky/__init__.py b/slinky/__init__.py index 05f18eb..86984cf 100644 --- a/slinky/__init__.py +++ b/slinky/__init__.py @@ -6,9 +6,6 @@ import random import string from dataclasses import dataclass from datetime import datetime -from typing import Optional - -import sqlalchemy # type: ignore[import] from slinky import db @@ -38,7 +35,7 @@ def random_string(length: int = 4) -> str: """ allowed_chars: str = string.ascii_letters + string.digits - return ''.join(random.SystemRandom().choice(allowed_chars) for _ in range(length)) + return "".join(random.SystemRandom().choice(allowed_chars) for _ in range(length)) class Slinky: @@ -52,8 +49,8 @@ class Slinky: def add( # pylint: disable=too-many-arguments self, - shortcode: str = '', - url: str = '', + shortcode: str = "", + url: str = "", length: int = 4, fixed_views: int = -1, expiry: datetime = datetime.max, @@ -78,7 +75,7 @@ class Slinky: shortcode = random_string(length=length) if self.get_by_shortcode(shortcode).url: - raise ValueError(f'Shortcode {shortcode} already exists') + raise ValueError(f"Shortcode {shortcode} already exists") dbentry = db.ShortURL( shortcode=shortcode, @@ -114,7 +111,7 @@ class Slinky: ) self.session.close() return ret_sc - return Shortcode(0, '', '', 0, '1970-01-01 00:00:00.000000') + return Shortcode(0, "", "", 0, "1970-01-01 00:00:00.000000") def remove_view(self, sc_id: int) -> None: """ diff --git a/slinky/db.py b/slinky/db.py index d286de4..92f1073 100644 --- a/slinky/db.py +++ b/slinky/db.py @@ -1,21 +1,22 @@ """ DB component """ + from dataclasses import dataclass -from sqlalchemy import Column, Integer, String, create_engine # type: ignore[import] -from sqlalchemy.ext.declarative import declarative_base # type: ignore[import] -from sqlalchemy.orm import Session, sessionmaker # type: ignore[import] +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Session, sessionmaker Base = declarative_base() -class ShortURL(Base): # type: ignore[misc, valid-type] # pylint: disable=too-few-public-methods +class ShortURL(Base): # pylint: disable=too-few-public-methods """ Class to describe the DB schema for ShortURLs """ - __tablename__ = 'shorturl' + __tablename__ = "shorturl" id = Column(Integer, primary_key=True) shortcode = Column(String(128), unique=True, nullable=False) diff --git a/slinky/web.py b/slinky/web.py index 14b0cd3..a9c010a 100644 --- a/slinky/web.py +++ b/slinky/web.py @@ -10,71 +10,70 @@ from typing import Any, Callable import yaml from flask import Blueprint, Response, render_template, request -from flask_wtf import FlaskForm # type: ignore[import] -from wtforms import HiddenField # type: ignore[import] -from wtforms import DateTimeLocalField, IntegerField, StringField -from wtforms.validators import DataRequired, Length # type: ignore[import] +from flask_wtf import FlaskForm +from wtforms import DateTimeLocalField, HiddenField, IntegerField, StringField +from wtforms.validators import DataRequired, Length from slinky import Slinky, random_string -slinky_webapp = Blueprint('webapp', __name__, template_folder='templates') +slinky_webapp = Blueprint("webapp", __name__, template_folder="templates") -with open('config.yaml', encoding='utf-8-sig') as conffile: +with open("config.yaml", encoding="utf-8-sig") as conffile: cfg = yaml.safe_load(conffile) -class DelForm(FlaskForm): # type: ignore[misc] +class DelForm(FlaskForm): """ Delete form definition """ - delete = HiddenField('delete') + delete = HiddenField("delete") -class AddForm(FlaskForm): # type: ignore[misc] +class AddForm(FlaskForm): """ Add form definition """ shortcode = StringField( - 'Shortcode', + "Shortcode", validators=[DataRequired(), Length(1, 2048)], render_kw={ - 'size': 64, - 'maxlength': 2048, + "size": 64, + "maxlength": 2048, }, ) url = StringField( - 'URL', + "URL", validators=[DataRequired(), Length(1, 2048)], render_kw={ - 'size': 64, - 'maxlength': 2048, - 'placeholder': 'https://www.example.com', + "size": 64, + "maxlength": 2048, + "placeholder": "https://www.example.com", }, ) fixed_views = IntegerField( - 'Fixed number of views', + "Fixed number of views", validators=[DataRequired()], render_kw={ - 'size': 3, - 'value': -1, + "size": 3, + "value": -1, }, ) length = IntegerField( - 'Shortcode length', + "Shortcode length", validators=[DataRequired()], render_kw={ - 'size': 3, - 'value': 4, + "size": 3, + "value": 4, }, ) expiry = DateTimeLocalField( - 'Expiry', - format='%Y-%m-%dT%H:%M', + "Expiry", + format="%Y-%m-%dT%H:%M", render_kw={ - 'size': 8, - 'maxlength': 10, + "size": 8, + "maxlength": 10, }, ) @@ -92,18 +91,26 @@ def protect(func: Callable[..., Response]) -> Callable[..., Response]: @wraps(func) def check_ip(*args: Any, **kwargs: Any) -> Response: + remote_addr = request.remote_addr + + if "x-forwarded-for" in request.headers: + remote_addr = request.headers["x-forwarded-for"] + + if "x-real-ip" in request.headers: + remote_addr = request.headers["x-real-ip"] + if ( - os.environ.get('FLASK_ENV', '') != 'development' - and request.headers['X-Forwarded-For'] not in cfg['allowed_ips'] + os.environ.get("FLASK_ENV", "") != "development" + and remote_addr not in cfg["allowed_ips"] ): - logging.warning('Protected URL access attempt from %s', request.remote_addr) - return Response('Not found', 404) + logging.warning("Protected URL access attempt from %s", remote_addr) + return Response("Not found", 404) return func(*args, **kwargs) return check_ip -@slinky_webapp.route('/', strict_slashes=False) +@slinky_webapp.route("/", strict_slashes=False) def try_path_as_shortcode(path: str) -> Response: """ Try the initial path as a shortcode, redirect if found @@ -112,27 +119,27 @@ def try_path_as_shortcode(path: str) -> Response: Response: redirect if found, otherwise 404 """ should_redirect = True - slinky = Slinky(cfg['db']) + slinky = Slinky(cfg["db"]) shortcode = slinky.get_by_shortcode(path) if shortcode.url: if shortcode.fixed_views == 0: - logging.warning('Shortcode out of views') + logging.warning("Shortcode out of views") should_redirect = False elif shortcode.fixed_views > 0: slinky.remove_view(shortcode.id) if datetime.fromisoformat(shortcode.expiry) < datetime.now(): - logging.warning('Shortcode expired') + logging.warning("Shortcode expired") should_redirect = False if should_redirect: return Response( - 'Redirecting...', status=302, headers={'location': shortcode.url} + "Redirecting...", status=302, headers={"location": shortcode.url} ) - return Response('Not found', 404) + return Response("Not found", 404) -@slinky_webapp.route('/_/add', methods=['GET', 'POST']) +@slinky_webapp.route("/_/add", methods=["GET", "POST"]) @protect def add() -> Response: """ @@ -141,27 +148,27 @@ def add() -> Response: Returns: Response: HTTP response """ - slinky = Slinky(cfg['db']) + slinky = Slinky(cfg["db"]) for attempts in range(50): shortcode = random_string() if slinky.get_by_shortcode(shortcode).url: logging.warning( - 'Shortcode already exists. Retrying (%s/50).', + "Shortcode already exists. Retrying (%s/50).", attempts, ) else: break else: return Response( - render_template('error.html', msg='Could not create a unique shortcode'), + render_template("error.html", msg="Could not create a unique shortcode"), 500, ) - url = '' - final_url = '' + url = "" + final_url = "" - form = AddForm(meta={'csrf': False}) + form = AddForm(meta={"csrf": False}) if form.is_submitted(): shortcode = form.shortcode.data.strip() @@ -170,25 +177,25 @@ def add() -> Response: fixed_views = form.fixed_views.data expiry = form.expiry.data or datetime.max - if url: - try: - shortcode = slinky.add( - shortcode=shortcode, - url=url, - length=length, - fixed_views=fixed_views, - expiry=expiry, - ) - except ValueError as error: - logging.warning(error) - return Response(render_template('error.html', msg=error), 400) + if url: + try: + shortcode = slinky.add( + shortcode=shortcode, + url=url, + length=length, + fixed_views=fixed_views, + expiry=expiry, + ) + except ValueError as error: + logging.warning(error) + return Response(render_template("error.html", msg=error), 400) - if form.is_submitted(): - final_url = f'{request.host_url}/{shortcode}' + if form.is_submitted(): + final_url = f"{request.host_url}/{shortcode}" return Response( render_template( - 'add.html', + "add.html", form=form, shortcode=shortcode, final_url=final_url, @@ -197,7 +204,7 @@ def add() -> Response: ) -@slinky_webapp.route('/_/list', methods=['GET', 'POST']) +@slinky_webapp.route("/_/list", methods=["GET", "POST"]) @protect def lister() -> Response: """ @@ -206,19 +213,19 @@ def lister() -> Response: Returns: Response: HTTP response """ - form = DelForm(meta={'csrf': False}) - slinky = Slinky(cfg['db']) + form = DelForm(meta={"csrf": False}) + slinky = Slinky(cfg["db"]) if form.is_submitted(): slinky.delete_by_shortcode(form.delete.data.strip()) return Response( - render_template('list.html', form=form, shortcodes=slinky.get_all()), + render_template("list.html", form=form, shortcodes=slinky.get_all()), 200, ) -@slinky_webapp.route('/_/edit/', methods=['GET', 'POST']) +@slinky_webapp.route("/_/edit/", methods=["GET", "POST"]) @protect def edit(id: int) -> Response: # pylint: disable=invalid-name,redefined-builtin """ @@ -227,15 +234,15 @@ def edit(id: int) -> Response: # pylint: disable=invalid-name,redefined-builtin Returns: Response: HTTP response """ - form = DelForm(meta={'csrf': False}) - slinky = Slinky(cfg['db']) + form = DelForm(meta={"csrf": False}) + slinky = Slinky(cfg["db"]) - logging.debug('Editing: %d', id) + logging.debug("Editing: %d", id) if form.is_submitted(): slinky.delete_by_shortcode(form.delete.data.strip()) return Response( - render_template('edit.html', form=form, shortcodes=slinky.get_all()), + render_template("edit.html", form=form, shortcodes=slinky.get_all()), 200, ) diff --git a/templates/_head.html b/templates/_head.html index 23ce692..7ade78d 100644 --- a/templates/_head.html +++ b/templates/_head.html @@ -1,98 +1,97 @@ - - - - + + + + - Slinky + Slinky - + - - - - + + + - + [role=status] { + display: none; + } - - - - + .tooltip>.tooltip-inner { + max-width: 60em !important; + text-align: left !important; + } - - + diff --git a/templates/_tail.html b/templates/_tail.html index d42b00e..a0d57e5 100644 --- a/templates/_tail.html +++ b/templates/_tail.html @@ -1,6 +1,6 @@ - +