commit 88f1345fb31ad3a41fbf3efb892ae68257d8361e Author: Kevin Trocolli Date: Thu May 23 00:41:14 2024 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b971528 --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +logs/* +*.txt +*.json \ No newline at end of file diff --git a/index.py b/index.py new file mode 100644 index 0000000..72ea095 --- /dev/null +++ b/index.py @@ -0,0 +1,156 @@ +from starlette.applications import Starlette +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import FileResponse, Response, RedirectResponse +from os import path, mkdir, W_OK, access +from base64 import b64encode, b64decode +from random import choices +from string import ascii_letters, digits, punctuation +from sys import argv +from typing import Dict +from logging.handlers import TimedRotatingFileHandler +import logging +import coloredlogs +import json + +if not path.exists("logs"): + mkdir("logs") + +if not access("logs", W_OK): + print(f"Log directory NOT writable, please check permissions") + exit(1) + +logger = logging.getLogger("access") +log_fmt_str = "[%(asctime)s] Access | %(levelname)s | %(message)s" +log_fmt = logging.Formatter(log_fmt_str) +fileHandler = TimedRotatingFileHandler( + "logs/access.log", + when="d", + backupCount=10, +) +fileHandler.setFormatter(log_fmt) + +consoleHandler = logging.StreamHandler() +consoleHandler.setFormatter(log_fmt) + +logger.addHandler(fileHandler) +logger.addHandler(consoleHandler) + +logger.setLevel(logging.INFO) +coloredlogs.install( + level=logging.INFO, logger=logger, fmt=log_fmt_str +) + +if not path.exists("flist.json"): + logger.warning("Regenerate file list") + flist = {} + with open("flist.json", "w") as f: + f.write("{}") + +else: + with open("flist.json", "r") as f: + flist = json.load(f) + +if not path.exists("admin.txt"): + admin_creds = None + +else: + with open("admin.txt", "r") as f: + admin_creds = f.read() + + try: + admin_decode = b64decode(admin_creds.encode()).decode() + if not admin_decode or len(admin_decode) < 14 or ":" not in admin_decode: + logger.warning(f"Invalid admin creds {admin_decode} - regenerating") + admin_creds = None + + except Exception as e: + logger.warning(f"Failed to decode {admin_creds} as b64, regenerating - {e}") + admin_creds = None + +if not admin_creds: + pw = "".join(choices(ascii_letters + digits + punctuation, k=16)) + print(f"Generate password for admin: {pw}") # no logging + admin_creds = b64encode(f"admin:{pw}".encode()).decode() + with open("admin.txt", "w") as f: + f.write(admin_creds) + +def get_ip_addr(req: Request) -> str: + ip = req.headers.get("x-forwarded-for", req.client.host) + return ip.split(", ")[0] + +async def handle_file(request: Request) -> FileResponse: + name = request.path_params.get("name", "") + key = request.path_params.get("key", "") + req_ip = get_ip_addr(request) + if not name or not key: + return Response(status_code=400) + + info: Dict = flist.get("name", {}) + if not info: + return Response(status_code=404) + + if info.get("key", "") != key: + logger.info(f"Incorrect key for file {name} from {req_ip}") + return Response(status_code=404) + + fpath: str = info.get("path", "") + if not fpath or not path.exists(fpath): + logger.info(f"File {name} does not exist at {fpath}") + return Response(status_code=500) + + if fpath.endswith("/") or fpath.endswith("\\"): + logger.info(f"Cannot send directory {fpath} as file {name}") + return Response(status_code=500) + + last_slash_idx = fpath.rfind("/") + if last_slash_idx == -1: + last_slash_idx = fpath.rfind("\\") + + if last_slash_idx == -1: + last_slash_idx = fpath.rfind("\\\\") + + if last_slash_idx == -1: + fname = fpath + + if last_slash_idx > -1: + fname = fpath[last_slash_idx + 1:] + + logger.info(f"Send {name} ({fpath}) to {req_ip}") + return FileResponse(fpath, filename=fname) + +async def handle_upload(request: Request) -> Response: + return Response("Uploading not currently supported", status_code=404) + +async def render_admin(request: Request) -> Response: + pass + +async def render_login(request: Request) -> Response: + pass + +async def handle_login(request: Request) -> RedirectResponse: + pass + +async def add_file(request: Request) -> RedirectResponse: + pass + +async def del_file(request: Request) -> RedirectResponse: + pass + +async def chg_pw(request: Request) -> RedirectResponse: + pass + +rt_lst = [ + Route("/file/{key:str}/{name:str}", handle_file, methods=['GET']), + Route("/upload/{key:str}/{name:str}", handle_upload, methods=['PUT']), + Mount("/admin", routes=[ + Route("/", render_admin, methods=['GET']), + Route("/login", render_login, methods=['GET']), + Route("/file.add", add_file, methods=['POST']), + Route("/file.del", del_file, methods=['POST']), + Route("/admin.pw", chg_pw, methods=['POST']), + Route("/admin.login", handle_login, methods=['POST']), + ]) +] + +app = Starlette("debug" in argv, rt_lst) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5fe0ae5 --- /dev/null +++ b/readme.md @@ -0,0 +1,2 @@ +# HayCDN +Very simple (some would say barebones) CDN based on starlette. \ No newline at end of file