client+server: implement HTML caching and URL prefix determination
* Uses 'X-Forwarded-Prefix' header to determine base URL if not explicitly defined * Authentication ignored for HTML generation to improve caching * Also add logging of 'User-Agent' for HTML requests
This commit is contained in:
parent
d5a223652e
commit
636498ad38
8 changed files with 108 additions and 48 deletions
|
@ -1,5 +1,9 @@
|
|||
#!/usr/bin/dumb-init /bin/sh
|
||||
|
||||
# Create cache directory
|
||||
mkdir -p /tmp/nginx-cache
|
||||
chmod a+rwx /tmp/nginx-cache
|
||||
|
||||
# Integrate environment variables
|
||||
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||
/etc/nginx/nginx.conf
|
||||
|
|
|
@ -19,6 +19,9 @@ http {
|
|||
server_tokens off;
|
||||
keepalive_timeout 65;
|
||||
|
||||
proxy_cache_path /tmp/nginx-cache
|
||||
levels=1:2 keys_zone=spa_cache:1m max_size=50m inactive=60m use_temp_path=off;
|
||||
|
||||
upstream backend {
|
||||
server __BACKEND__:6666;
|
||||
}
|
||||
|
@ -50,6 +53,14 @@ http {
|
|||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain application/json;
|
||||
|
||||
if ($http_x_forwarded_host = '') {
|
||||
set $http_x_forwarded_host $host;
|
||||
}
|
||||
|
||||
if ($http_x_forwarded_proto = '') {
|
||||
set $http_x_forwarded_proto 'http';
|
||||
}
|
||||
|
||||
if ($request_uri ~* "/api/(.*)") {
|
||||
proxy_pass http://backend/$1;
|
||||
}
|
||||
|
@ -85,6 +96,29 @@ http {
|
|||
location / {
|
||||
tcp_nodelay on;
|
||||
|
||||
proxy_cache spa_cache;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_lock on;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 3;
|
||||
gzip_min_length 20;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain application/json;
|
||||
|
||||
if ( $http_accept ~ "application/json" ) {
|
||||
return 406 "API requests should be sent to the /api prefix";
|
||||
}
|
||||
|
||||
if ($http_x_forwarded_host = '') {
|
||||
set $http_x_forwarded_host $host;
|
||||
}
|
||||
|
||||
if ($http_x_forwarded_proto = '') {
|
||||
set $http_x_forwarded_proto 'http';
|
||||
}
|
||||
|
||||
proxy_pass http://backend;
|
||||
|
||||
error_page 500 502 503 504 @badproxy;
|
||||
|
|
|
@ -74,20 +74,17 @@ def generate_manifest(
|
|||
"name": config.config["name"],
|
||||
"icons": [
|
||||
{
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
"src": "img/android-chrome-192x192.png",
|
||||
"src": f"{ctx.url_prefix}/img/android-chrome-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
},
|
||||
{
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
"src": "img/android-chrome-512x512.png",
|
||||
"src": f"{ctx.url_prefix}/img/android-chrome-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
},
|
||||
],
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
"start_url": "/",
|
||||
"start_url": f"{ctx.url_prefix}/",
|
||||
"theme_color": "#24aadd",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
|
|
|
@ -62,7 +62,7 @@ _apple_touch_startup_images = {
|
|||
|
||||
|
||||
def _get_html_template(
|
||||
meta_tags: Dict = {}, title: str = config.config["name"]
|
||||
meta_tags: Dict = {}, title: str = config.config["name"], prefix: str = ""
|
||||
) -> Doc:
|
||||
doc = Doc()
|
||||
doc.asis("<!DOCTYPE html>")
|
||||
|
@ -73,18 +73,19 @@ def _get_html_template(
|
|||
doc.stag("meta", name=name, content=content)
|
||||
with doc.tag("title"):
|
||||
doc.text(title)
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
doc.stag("base", href="/")
|
||||
doc.stag("link", rel="manifest", href="/manifest.json")
|
||||
doc.stag("base", href=f"{prefix}/")
|
||||
doc.stag(
|
||||
"link", rel="manifest", href=f"{prefix}/api/manifest.json"
|
||||
)
|
||||
doc.stag(
|
||||
"link",
|
||||
href="css/app.min.css",
|
||||
href=f"{prefix}/css/app.min.css",
|
||||
rel="stylesheet",
|
||||
type="text/css",
|
||||
)
|
||||
doc.stag(
|
||||
"link",
|
||||
href="css/vendor.min.css",
|
||||
href=f"{prefix}/css/vendor.min.css",
|
||||
rel="stylesheet",
|
||||
type="text/css",
|
||||
)
|
||||
|
@ -92,19 +93,19 @@ def _get_html_template(
|
|||
"link",
|
||||
rel="shortcut icon",
|
||||
type="image/png",
|
||||
href="img/favicon.png",
|
||||
href=f"{prefix}/img/favicon.png",
|
||||
)
|
||||
doc.stag(
|
||||
"link",
|
||||
rel="apple-touch-icon",
|
||||
sizes="180x180",
|
||||
href="img/apple-touch-icon.png",
|
||||
href=f"{prefix}/img/apple-touch-icon.png",
|
||||
)
|
||||
for res, media in _apple_touch_startup_images.items():
|
||||
doc.stag(
|
||||
"link",
|
||||
rel="apple-touch-startup-image",
|
||||
href=f"img/apple-touch-startup-image-{res}.png",
|
||||
href=f"{prefix}/img/apple-touch-startup-image-{res}.png",
|
||||
media=" and ".join(
|
||||
f"({k}: {v})" for k, v in media.items()
|
||||
),
|
||||
|
@ -148,21 +149,25 @@ def get_post_html(
|
|||
except posts.InvalidPostIdError:
|
||||
# Return the default template and let the browser JS handle the 404
|
||||
return _get_html_template()
|
||||
|
||||
metadata = {
|
||||
"og:site_name": config.config["name"],
|
||||
"og:type": "image",
|
||||
"og:title": title,
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
"og:url": f"{config.config['domain'] or ''}/post/{params['post_id']}",
|
||||
"og:url": f"{ctx.url_prefix}/post/{params['post_id']}",
|
||||
}
|
||||
# Note: ctx.user will always be the anonymous user
|
||||
if auth.has_privilege(ctx.user, "posts:view"):
|
||||
# TODO: Host Header and Proxy Prefix
|
||||
metadata["og:image"] = posts.get_post_content_url(post)
|
||||
return _get_html_template(meta_tags=metadata, title=title)
|
||||
|
||||
return _get_html_template(
|
||||
meta_tags=metadata, title=title, prefix=ctx.url_prefix
|
||||
)
|
||||
|
||||
|
||||
@rest.routes.get("/.*", accept="text/html")
|
||||
def default_route(
|
||||
ctx: rest.Context, _params: Dict[str, str] = {}
|
||||
) -> rest.Response:
|
||||
return _get_html_template()
|
||||
return _get_html_template(prefix=ctx.url_prefix)
|
||||
|
|
|
@ -73,10 +73,11 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]:
|
|||
|
||||
def process_request(ctx: rest.Context) -> None:
|
||||
""" Bind the user to request. Update last login time if needed. """
|
||||
bump_login = ctx.get_param_as_bool("bump-login", default=False)
|
||||
auth_user = _get_user(ctx, bump_login)
|
||||
if auth_user:
|
||||
ctx.user = auth_user
|
||||
if ctx.accept == "application/json":
|
||||
bump_login = ctx.get_param_as_bool("bump-login", default=False)
|
||||
auth_user = _get_user(ctx, bump_login)
|
||||
if auth_user:
|
||||
ctx.user = auth_user
|
||||
|
||||
|
||||
@rest.middleware.pre_hook
|
||||
|
|
|
@ -13,10 +13,18 @@ def process_request(_ctx: rest.Context) -> None:
|
|||
|
||||
@middleware.post_hook
|
||||
def process_response(ctx: rest.Context) -> None:
|
||||
logger.info(
|
||||
"%s %s (user=%s, queries=%d)",
|
||||
ctx.method,
|
||||
ctx.url,
|
||||
ctx.user.name,
|
||||
db.get_query_count(),
|
||||
)
|
||||
if ctx.accept == "application/json":
|
||||
logger.info(
|
||||
"%s %s (user=%s, queries=%d)",
|
||||
ctx.method,
|
||||
ctx.url,
|
||||
ctx.user.name,
|
||||
db.get_query_count(),
|
||||
)
|
||||
elif ctx.accept == "text/html":
|
||||
logger.info(
|
||||
"HTML %s (user-agent='%s' queries=%d)",
|
||||
ctx.url,
|
||||
ctx.get_header("User-Agent"),
|
||||
db.get_query_count(),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import urllib.parse
|
|||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, Tuple
|
||||
|
||||
from szurubooru import db
|
||||
from szurubooru import config, db
|
||||
from szurubooru.func import util
|
||||
from szurubooru.rest import context, errors, middleware, routes
|
||||
|
||||
|
@ -40,6 +40,23 @@ def _create_context(env: Dict[str, Any]) -> context.Context:
|
|||
path = "/" + env["PATH_INFO"].lstrip("/")
|
||||
path = path.encode("latin-1").decode("utf-8") # PEP-3333
|
||||
headers = _get_headers(env)
|
||||
_raw_accept = headers.get("Accept", "text/html")
|
||||
|
||||
if "application/json" in _raw_accept:
|
||||
accept = "application/json"
|
||||
elif "text/html" in _raw_accept:
|
||||
accept = "text/html"
|
||||
else:
|
||||
raise errors.HttpNotAcceptable(
|
||||
"ValidationError",
|
||||
"This API only supports the following response types: "
|
||||
"application/json, text/html",
|
||||
)
|
||||
|
||||
if config.config["domain"]:
|
||||
url_prefix = config.config["domain"].rstrip("/")
|
||||
else:
|
||||
url_prefix = headers.get("X-Forwarded-Prefix", "")
|
||||
|
||||
files = {}
|
||||
params = dict(urllib.parse.parse_qsl(env.get("QUERY_STRING", "")))
|
||||
|
@ -70,27 +87,17 @@ def _create_context(env: Dict[str, Any]) -> context.Context:
|
|||
"was incorrect or was not encoded as UTF-8.",
|
||||
)
|
||||
|
||||
return context.Context(env, method, path, headers, params, files)
|
||||
return context.Context(
|
||||
env, method, path, headers, accept, url_prefix, params, files
|
||||
)
|
||||
|
||||
|
||||
def application(
|
||||
env: Dict[str, Any], start_response: Callable[[str, Any], Any]
|
||||
) -> Tuple[bytes]:
|
||||
accept = None
|
||||
try:
|
||||
ctx = _create_context(env)
|
||||
if "application/json" in ctx.get_header("Accept"):
|
||||
accept = "application/json"
|
||||
elif "text/html" in (ctx.get_header("Accept") or "text/html"):
|
||||
accept = "text/html"
|
||||
else:
|
||||
raise errors.HttpNotAcceptable(
|
||||
"ValidationError",
|
||||
"This API only supports the following response types: "
|
||||
", ".join(routes.routes.keys()),
|
||||
)
|
||||
|
||||
for url, allowed_methods in routes.routes[accept].items():
|
||||
for url, allowed_methods in routes.routes[ctx.accept].items():
|
||||
match = re.fullmatch(url, ctx.url)
|
||||
if match:
|
||||
if ctx.method not in allowed_methods:
|
||||
|
@ -122,9 +129,9 @@ def application(
|
|||
finally:
|
||||
db.session.remove()
|
||||
|
||||
start_response("200", [("content-type", accept)])
|
||||
start_response("200", [("content-type", ctx.accept)])
|
||||
return (
|
||||
_serialize_response_body(response, accept).encode("utf-8"),
|
||||
_serialize_response_body(response, ctx.accept).encode("utf-8"),
|
||||
)
|
||||
|
||||
except Exception as ex:
|
||||
|
@ -136,7 +143,7 @@ def application(
|
|||
except errors.BaseHttpError as ex:
|
||||
start_response(
|
||||
"%d %s" % (ex.code, ex.reason),
|
||||
[("content-type", accept or "application/json")],
|
||||
[("content-type", "application/json")],
|
||||
)
|
||||
blob = {
|
||||
"name": ex.name,
|
||||
|
|
|
@ -15,12 +15,16 @@ class Context:
|
|||
method: str,
|
||||
url: str,
|
||||
headers: Dict[str, str] = None,
|
||||
accept: str = None,
|
||||
url_prefix: str = None,
|
||||
params: Request = None,
|
||||
files: Dict[str, bytes] = None,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.accept = accept
|
||||
self.url_prefix = url_prefix
|
||||
self._headers = headers or {}
|
||||
self._params = params or {}
|
||||
self._files = files or {}
|
||||
|
|
Loading…
Reference in a new issue