2023-10-23 02:51:39 +01:00
|
|
|
import mimetypes
|
2023-10-15 05:31:54 +01:00
|
|
|
from importlib.resources import files
|
2023-10-23 02:51:39 +01:00
|
|
|
from urllib.parse import unquote
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-11-01 14:53:57 +00:00
|
|
|
import asyncio
|
2023-11-01 14:03:17 +00:00
|
|
|
import brotli
|
2023-10-23 02:51:39 +01:00
|
|
|
from sanic import Blueprint, Sanic, raw
|
|
|
|
from sanic.exceptions import Forbidden, NotFound
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-23 02:51:39 +01:00
|
|
|
from cista import auth, config, session, watching
|
|
|
|
from cista.api import bp
|
2023-10-21 20:30:47 +01:00
|
|
|
from cista.util import filename
|
2023-10-23 02:51:39 +01:00
|
|
|
from cista.util.apphelpers import handle_sanic_exception
|
|
|
|
|
|
|
|
app = Sanic("cista", strict_slashes=True)
|
|
|
|
app.blueprint(auth.bp)
|
|
|
|
app.blueprint(bp)
|
|
|
|
app.exception(Exception)(handle_sanic_exception)
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-28 21:20:34 +01:00
|
|
|
|
2023-10-23 02:51:39 +01:00
|
|
|
@app.before_server_start
|
|
|
|
async def main_start(app, loop):
|
|
|
|
config.load_config()
|
|
|
|
await watching.start(app, loop)
|
|
|
|
|
2023-10-28 21:20:34 +01:00
|
|
|
|
2023-10-23 02:51:39 +01:00
|
|
|
@app.after_server_stop
|
|
|
|
async def main_stop(app, loop):
|
|
|
|
await watching.stop(app, loop)
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-28 21:20:34 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
@app.on_request
|
2023-10-23 02:51:39 +01:00
|
|
|
async def use_session(req):
|
|
|
|
req.ctx.session = session.get(req)
|
|
|
|
try:
|
2023-10-23 23:47:57 +01:00
|
|
|
req.ctx.user = config.config.users[req.ctx.session["username"]] # type: ignore
|
2023-10-23 02:51:39 +01:00
|
|
|
except (AttributeError, KeyError, TypeError):
|
|
|
|
req.ctx.user = None
|
2023-10-21 02:44:43 +01:00
|
|
|
# CSRF protection
|
2023-10-23 02:51:39 +01:00
|
|
|
if req.method == "GET" and req.headers.upgrade != "websocket":
|
2023-10-21 02:44:43 +01:00
|
|
|
return # Ordinary GET requests are fine
|
2023-10-23 02:51:39 +01:00
|
|
|
# Check that origin matches host, for browsers which should all send Origin.
|
|
|
|
# Curl doesn't send any Origin header, so we allow it anyway.
|
|
|
|
origin = req.headers.origin
|
|
|
|
if origin and origin.split("//", 1)[1] != req.host:
|
2023-10-21 02:44:43 +01:00
|
|
|
raise Forbidden("Invalid origin: Cross-Site requests not permitted")
|
|
|
|
|
2023-10-28 21:20:34 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
@app.before_server_start
|
2023-10-23 02:51:39 +01:00
|
|
|
def http_fileserver(app, _):
|
|
|
|
bp = Blueprint("fileserver")
|
|
|
|
bp.on_request(auth.verify)
|
2023-10-28 21:20:34 +01:00
|
|
|
bp.static(
|
|
|
|
"/files/",
|
|
|
|
config.config.path,
|
|
|
|
use_content_range=True,
|
|
|
|
stream_large_files=True,
|
|
|
|
directory_view=True,
|
|
|
|
)
|
2023-10-23 02:51:39 +01:00
|
|
|
app.blueprint(bp)
|
|
|
|
|
2023-10-28 21:20:34 +01:00
|
|
|
|
2023-11-01 14:03:17 +00:00
|
|
|
www = {}
|
|
|
|
|
|
|
|
|
|
|
|
@app.before_server_start
|
2023-11-01 14:40:08 +00:00
|
|
|
def load_wwwroot(app):
|
2023-11-01 14:53:57 +00:00
|
|
|
global www
|
|
|
|
wwwnew = {}
|
2023-11-01 14:40:08 +00:00
|
|
|
base = files("cista") / "wwwroot"
|
|
|
|
paths = ["."]
|
|
|
|
while paths:
|
|
|
|
path = paths.pop(0)
|
|
|
|
current = base / path
|
2023-11-01 14:03:17 +00:00
|
|
|
for p in current.iterdir():
|
|
|
|
if p.is_dir():
|
|
|
|
paths.append(current / p.parts[-1])
|
|
|
|
continue
|
|
|
|
name = p.relative_to(base).as_posix()
|
|
|
|
mime = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
|
|
|
data = p.read_bytes()
|
2023-11-01 14:53:57 +00:00
|
|
|
# Use old data if not changed
|
|
|
|
if name in www and www[name][0] == data:
|
|
|
|
wwwnew[name] = www[name]
|
|
|
|
continue
|
|
|
|
# Precompress with Brotli
|
2023-11-01 14:03:17 +00:00
|
|
|
br = brotli.compress(data)
|
|
|
|
if len(br) >= len(data):
|
|
|
|
br = False
|
2023-11-01 14:53:57 +00:00
|
|
|
wwwnew[name] = data, br, mime
|
|
|
|
www = wwwnew
|
|
|
|
|
|
|
|
@app.add_task
|
|
|
|
async def refresh_wwwroot():
|
|
|
|
while app.debug:
|
|
|
|
await asyncio.sleep(0.5)
|
|
|
|
load_wwwroot(app)
|
2023-11-01 14:03:17 +00:00
|
|
|
|
2023-10-23 02:51:39 +01:00
|
|
|
@app.get("/<path:path>", static=True)
|
|
|
|
async def wwwroot(req, path=""):
|
|
|
|
"""Frontend files only"""
|
2023-11-01 14:40:08 +00:00
|
|
|
name = unquote(path) or "index.html"
|
|
|
|
if name not in www:
|
|
|
|
raise NotFound(f"File not found: /{path}", extra={"name": name})
|
|
|
|
data, br, mime = www[name]
|
|
|
|
headers = {}
|
|
|
|
# Brotli compressed?
|
|
|
|
if br and "br" in req.headers.accept_encoding.split(", "):
|
|
|
|
headers["content-encoding"] = "br"
|
|
|
|
data = br
|
|
|
|
return raw(data, content_type=mime, headers=headers)
|