diff --git a/deploy/varnish/default.vcl.template b/deploy/varnish/default.vcl.template index b9aa5bc..b7113f9 100644 --- a/deploy/varnish/default.vcl.template +++ b/deploy/varnish/default.vcl.template @@ -3,17 +3,22 @@ vcl 4.1; import vsthrottle; import std; +# ===== Backend ===== backend app { .host = "app"; .port = "${APP_PORT}"; } -/* unikamy duplikatu; dodajemy IPv6 */ -acl purge { "127.0.0.1"; "::1"; } +# ===== ACL ===== +acl purge { + "127.0.0.1"; + "::1"; +} +# ===== RECV ===== sub vcl_recv { - # RATE LIMIT: 100 żądań / 10s, blokada 60s - if (vsthrottle.is_denied(client.identity, 100, 10s, 60s)) { + # RATE LIMIT: 50 żądań / 10s, blokada 60s + if (vsthrottle.is_denied(client.identity, 50, 10s, 60s)) { return (synth(429, "Too Many Requests")); } @@ -23,71 +28,163 @@ sub vcl_recv { return (purge); } - # omijamy cache dla healthchecków / wewn. nagłówka + # omijamy cache dla healthchecków / wewnętrznych nagłówków if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); } + # Specjalna obsługa WebSocket i socket.io + if (req.http.Upgrade ~ "(?i)websocket" || req.url ~ "^/socket.io/") { + return (pipe); + } + # metody inne niż GET/HEAD bez cache if (req.method != "GET" && req.method != "HEAD") { return (pass); } - # statyczne – agresywny cache - if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpg|svg|ico|woff2?)$") { + # ---- Normalizacja Accept-Encoding (kolejność preferencji: zstd > br > gzip) ---- + if (req.http.Accept-Encoding) { + if (req.http.Accept-Encoding ~ "zstd") { + set req.http.Accept-Encoding = "zstd"; + } else if (req.http.Accept-Encoding ~ "br") { + set req.http.Accept-Encoding = "br"; + } else if (req.http.Accept-Encoding ~ "gzip") { + set req.http.Accept-Encoding = "gzip"; + } else { + set req.http.Accept-Encoding = "identity"; + } + } + + # ---- (Opcjonalnie) Normalizacja Accept dla obrazów generowanych wariantowo ---- + # Jeśli backend renderuje różne formaty obrazów (webp/jpg) pod tym samym URL (bez rozszerzenia), + # można włączyć „dwustanowy” sygnał do hasha: + # + # if (req.url ~ "\.(png|jpe?g|gif|bmp)$") { + # if (req.http.Accept ~ "image/webp") { + # set req.http.X-Accept-Image = "modern"; # webp + # } else { + # set req.http.X-Accept-Image = "legacy"; # jpg/png + # } + # } + + # ---- STATYCZNE – agresywny cache + ignorujemy sesję ---- + if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") { + unset req.http.Cookie; + unset req.http.Authorization; return (hash); } return (hash); } +# ===== PIPE (WebSocket passthrough) ===== +sub vcl_pipe { + if (req.http.Upgrade) { + set bereq.http.Upgrade = req.http.Upgrade; + set bereq.http.Connection = req.http.Connection; + } +} + +# ===== HASH ===== +sub vcl_hash { + hash_data(req.url); + if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); } + + # Cookie: zostają dla dynamicznych (dla statyków wyczyszczone wcześniej) + if (req.http.Cookie) { hash_data(req.http.Cookie); } + + # Accept-Encoding: już znormalizowany do zstd/br/gzip/identity + if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); } + + # (Opcjonalnie) sygnał obrazów z negocjacją po Accept + if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); } +} + +# ===== BACKEND_RESPONSE ===== sub vcl_backend_response { - # Treści prywatne / zakazane do cache + # Zakaz cache – respektujemy if (beresp.http.Cache-Control ~ "(?i)no-store|private") { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } - # Preferuj s-maxage, potem max-age + # ---- STATYCZNE: zdejmij Set-Cookie i Vary: Cookie, zapewnij TTL ---- + if (bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") { + unset beresp.http.Set-Cookie; + + # Jeśli backend dodał Vary: Cookie, usuńmy ten element (nie wpływa na statyki) + if (beresp.http.Vary) { + set beresp.http.Vary = regsuball(beresp.http.Vary, "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", "\1"); + set beresp.http.Vary = regsuball(beresp.http.Vary, ",[[:space:]]*,", ","); + set beresp.http.Vary = regsub(beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", ""); + set beresp.http.Vary = regsub(beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", ""); + if (beresp.http.Vary ~ "^[[:space:]]*$") { unset beresp.http.Vary; } + } + + # Jeśli brak kontroli czasu życia – ustawiamy twarde wartości + if (!(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) { + set beresp.ttl = 24h; + set beresp.http.Cache-Control = "public, max-age=86400, immutable"; + } + + set beresp.grace = 1h; + set beresp.keep = 24h; + } + + # ---- Ogólne TTL z nagłówków ---- if (beresp.http.Cache-Control ~ "(?i)s-maxage=([0-9]+)") { set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*s-maxage=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Cache-Control ~ "(?i)max-age=([0-9]+)") { set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*max-age=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Expires) { - # fallback na Expires set beresp.ttl = std.time(beresp.http.Expires, now) - now; if (beresp.ttl < 0s) { set beresp.ttl = 0s; } } else { - # ostateczny fallback - set beresp.ttl = 60s; + if (beresp.ttl <= 0s) { set beresp.ttl = 60s; } } - # Jeśli immutable – zwiększ grace/keep + # Immutable => dłuższe grace/keep if (beresp.http.Cache-Control ~ "(?i)immutable") { set beresp.grace = 1h; set beresp.keep = 24h; } - # statykom daj minimalne TTL, gdy backend NIE ustawił CC - if ((bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpg|svg|ico|woff2?)$") - && !(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) { - set beresp.ttl = 24h; + # Kompresja po stronie Varnisha wyłącznie dla klientów akceptujących gzip + # i tylko jeśli backend nie dostarczył już Content-Encoding. + if (!beresp.http.Content-Encoding && bereq.http.Accept-Encoding ~ "gzip") { + # Kompresujemy tylko „tekstowe” typy + if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml|wasm)") { + set beresp.do_gzip = true; + } + } + + # Duże odpowiedzi streamujemy + if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { + set beresp.do_stream = true; } } +# ===== DELIVER ===== sub vcl_deliver { - if (obj.hits > 0) { + if (obj.uncacheable) { + set resp.http.X-Cache = "PASS"; + unset resp.http.Age; + } else if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; - # ukryj niepotrzebny nagłówek z MISS - #} else { - # set resp.http.X-Cache = "MISS"; + } else { + set resp.http.X-Cache = "MISS"; + unset resp.http.Age; } - # Nagłówki rate limit – MUSZĄ używać tej samej czwórki parametrów co is_denied() - #set resp.http.X-RateLimit-Limit = "100"; - #set resp.http.X-RateLimit-Window = "10s"; - #set resp.http.X-RateLimit-Remaining = vsthrottle.remaining(client.identity, 100, 10s, 60s); - #set resp.http.Retry-After = vsthrottle.blocked(client.identity, 100, 10s, 60s); - unset resp.http.Via; unset resp.http.X-Varnish; - #unset resp.http.Age; unset resp.http.Server; } + +sub vcl_synth { + set resp.http.X-Cache = "SYNTH"; +} + +# ===== PURGE HANDLER ===== +sub vcl_purge { + return (synth(200, "Purged")); +} +