diff --git a/deploy/varnish/default.vcl.template b/deploy/varnish/default.vcl.template index dbae70e..1b384b7 100644 --- a/deploy/varnish/default.vcl.template +++ b/deploy/varnish/default.vcl.template @@ -1,9 +1,10 @@ + vcl 4.1; import vsthrottle; import std; -# ===== Backend ===== +# ===== BACKEND ===== backend app { .host = "app"; .port = "${APP_PORT}"; @@ -17,32 +18,49 @@ acl purge { # ===== RECV ===== sub vcl_recv { - # RATE LIMIT: 200 żądań / 10s, blokada 60s + # Wewnętrzne znaczniki - nie ufamy wartościom od klienta + unset req.http.X-Varnish-CS-Mode; + unset req.http.X-Varnish-CS-Entry; + + # Rate limit: 200 / 10s, blokada 60s if (vsthrottle.is_denied(client.identity, 200, 10s, 60s)) { return (synth(429, "Too Many Requests")); } # PURGE tylko lokalnie if (req.method == "PURGE") { - if (!client.ip ~ purge) { return (synth(405, "Not allowed")); } + if (!client.ip ~ purge) { + return (synth(405, "Not allowed")); + } return (purge); } - # omijamy cache dla healthchecków / wewnętrznych nagłówków - if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); } + if (!req.http.X-Forwarded-Proto) { + set req.http.X-Forwarded-Proto = "https"; + } - # Specjalna obsługa WebSocket i socket.io - if (req.http.Upgrade ~ "(?i)websocket" || req.url ~ "^/socket.io/") { + # Healthcheck / internal + if (req.url == "/healthcheck" || req.http.X-Internal-Check) { + return (pass); + } + + # WebSocket / 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); } + # Metody inne niż GET/HEAD + if (req.method != "GET" && req.method != "HEAD") { + return (pass); + } - # Żądania z Authorization nie są buforowane - if (req.http.Authorization) { return (pass); } + # Authorization + if (req.http.Authorization) { + return (pass); + } - # ---- Normalizacja Accept-Encoding (kolejność: zstd > br > gzip) ---- + # Accept-Encoding if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "zstd") { set req.http.Accept-Encoding = "zstd"; @@ -55,51 +73,17 @@ sub vcl_recv { } } - # ---- (Opcjonalnie) Normalizacja Accept dla obrazów generowanych wariantowo ---- - # 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?)$") { + # Statyczne zasoby + 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); } - if (!req.http.X-Forwarded-Proto) { - set req.http.X-Forwarded-Proto = "https"; - } - - if (req.url == "/healthcheck" || req.http.X-Internal-Check) { - set req.http.X-Pass-Reason = "internal"; - return (pass); - } - - if (req.method != "GET" && req.method != "HEAD") { - set req.http.X-Pass-Reason = "method"; - return (pass); - } - - if (req.http.Authorization) { - set req.http.X-Pass-Reason = "auth"; - return (pass); - } - - # jeśli chcesz PASS przy cookie: - # if (req.http.Cookie) { - # set req.http.X-Pass-Reason = "cookie"; - # return (pass); - # } - return (hash); } -# ===== PIPE (WebSocket passthrough) ===== +# ===== PIPE ===== sub vcl_pipe { if (req.http.Upgrade) { set bereq.http.Upgrade = req.http.Upgrade; @@ -110,142 +94,289 @@ sub vcl_pipe { # ===== 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); } + if (req.http.host) { + hash_data(req.http.host); + } else { + hash_data(server.ip); + } - # Accept-Encoding: już znormalizowany do zstd/br/gzip/identity - if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); } + if (req.http.Cookie) { + hash_data(req.http.Cookie); + } - # (Opcjonalnie) sygnał obrazów z negocjacją po Accept - if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); } + if (req.http.Accept-Encoding) { + hash_data(req.http.Accept-Encoding); + } + + if (req.http.X-Accept-Image) { + hash_data(req.http.X-Accept-Image); + } + + return (lookup); } -# ===== BACKEND_RESPONSE ===== +# ===== CACHE STATUS: HIT ===== +sub vcl_hit { + # Zawartość świeża lub serwowana z grace + if (obj.ttl >= 0s || obj.ttl + obj.grace > 0s) { + set req.http.X-Varnish-CS-Mode = "hit"; + } else { + # Obiekt wygasł i wymaga pobrania + set req.http.X-Varnish-CS-Mode = "stale"; + } + + # Dalsze decyzje pozostawiamy builtin VCL +} + +# ===== CACHE STATUS: MISS ===== +sub vcl_miss { + if (req.http.X-Varnish-CS-Mode != "stale") { + set req.http.X-Varnish-CS-Mode = "miss"; + } +} + +# ===== CACHE STATUS: PASS ===== +sub vcl_pass { + if (req.method != "GET" && req.method != "HEAD") { + set req.http.X-Varnish-CS-Mode = "method"; + } else { + set req.http.X-Varnish-CS-Mode = "bypass"; + } +} + +# ===== BACKEND FETCH ===== +sub vcl_backend_fetch { + # Nie przekazujemy znaczników do aplikacji + unset bereq.http.X-Varnish-CS-Mode; + unset bereq.http.X-Varnish-CS-Entry; +} + +# ===== BACKEND RESPONSE ===== sub vcl_backend_response { - # Zakaz cache – respektujemy + # Zabezpieczenie wewnętrznych nagłówków + unset beresp.http.X-Varnish-CS-Detail; + unset beresp.http.X-Varnish-Local-Error; + + # Respektuj no-store / private if (beresp.http.Cache-Control ~ "(?i)no-store|private") { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Pass-Reason = "no-store"; + set beresp.http.X-Varnish-CS-Detail = "uncacheable"; return (deliver); } - # NIE cache'uj redirectów do loginu (HTML) z backendu + # Redirecty bez cache if (beresp.status >= 300 && beresp.status < 400) { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Pass-Reason = "redirect"; + set beresp.http.X-Varnish-CS-Detail = "redirect"; return (deliver); } - # Nie cache'uj statyków, jeśli status ≠ 200 + # Statyczne: cache tylko HTTP 200 if (bereq.url ~ "^/static/" || - bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)($|\?)") { + bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { if (beresp.status != 200) { set beresp.uncacheable = true; set beresp.ttl = 0s; + set beresp.http.X-Varnish-CS-Detail = "invalid-static-status"; return (deliver); } } - # Jeśli pod .js przychodzi text/html — też nie cache'uj (to zwykle redirect/login) - if (bereq.url ~ "\.js(\?.*)?$" && beresp.http.Content-Type ~ "(?i)text/html") { + # HTML zamiast JS/CSS - nie cache'uj + if (bereq.url ~ "\.(js|css)(\?.*)?$" && + beresp.http.Content-Type ~ "(?i)text/html") { set beresp.uncacheable = true; set beresp.ttl = 0s; + set beresp.http.X-Varnish-CS-Detail = "html-instead-of-static"; return (deliver); } - # Wymuś poprawny Content-Type dla .js/.css, gdy backend zwróci HTML - if (bereq.url ~ "\.js(\?.*)?$") { - if (!beresp.http.Content-Type || beresp.http.Content-Type ~ "(?i)text/html") { - set beresp.http.Content-Type = "application/javascript; charset=utf-8"; - } - } - if (bereq.url ~ "\.css(\?.*)?$") { - if (!beresp.http.Content-Type || beresp.http.Content-Type ~ "(?i)text/html") { - set beresp.http.Content-Type = "text/css; charset=utf-8"; - } + # Content-Type przy brakującym nagłówku + if (bereq.url ~ "\.js(\?.*)?$" && + !beresp.http.Content-Type) { + set beresp.http.Content-Type = + "application/javascript; charset=utf-8"; } - # ---- STATYCZNE: zdejmij Set-Cookie i Vary: Cookie, zapewnij TTL ---- - if (bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") { + if (bereq.url ~ "\.css(\?.*)?$" && + !beresp.http.Content-Type) { + set beresp.http.Content-Type = + "text/css; charset=utf-8"; + } + + # ===== STATYCZNE ===== + 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) + # Usuń Vary: Cookie 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; } + 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.http.Cache-Control = + "public, max-age=86400, immutable"; } set beresp.grace = 1h; - set beresp.keep = 24h; + set beresp.keep = 24h; } - # ---- Ogólne TTL z nagłówków ---- + # ===== TTL ===== 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); + 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); + set beresp.ttl = std.duration( + regsub( + beresp.http.Cache-Control, + "(?i).*max-age=([0-9]+).*", + "\1" + ) + "s", 0s + ); } else if (beresp.http.Expires) { set beresp.ttl = std.time(beresp.http.Expires, now) - now; - if (beresp.ttl < 0s) { set beresp.ttl = 0s; } + + if (beresp.ttl < 0s) { + set beresp.ttl = 0s; + } } else { - if (beresp.ttl <= 0s) { set beresp.ttl = 60s; } + if (beresp.ttl <= 0s) { + set beresp.ttl = 60s; + } } - # Immutable => dłuższe grace/keep if (beresp.http.Cache-Control ~ "(?i)immutable") { set beresp.grace = 1h; - set beresp.keep = 24h; + set beresp.keep = 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; wykluczamy WASM - if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml)") { + # ===== GZIP ===== + if (!beresp.http.Content-Encoding && + bereq.http.Accept-Encoding ~ "gzip") { + + if (beresp.http.Content-Type ~ + "(?i)text/|application/(javascript|json|xml)") { set beresp.do_gzip = true; } } - # Duże odpowiedzi streamujemy - if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { + # ===== STREAMING ===== + if (beresp.http.Content-Length && + std.integer(beresp.http.Content-Length, 0) > 1048576) { set beresp.do_stream = true; } } -# (Opcjonalnie) Serwuj „stale” przy błędach backendu, jeśli jest obiekt w grace +# ===== BACKEND ERROR ===== sub vcl_backend_error { + # Odpowiedź wygenerowana lokalnie - bez Cache-Status + set beresp.http.X-Varnish-Local-Error = "1"; return (deliver); } -# ===== DELIVER ===== +# ===== DELIVER: RFC 9211 ===== sub vcl_deliver { - if (obj.uncacheable) { - if (req.http.X-Pass-Reason) { - set resp.http.X-Cache = "PASS:" + req.http.X-Pass-Reason; - } else if (resp.http.X-Pass-Reason) { # z backendu - set resp.http.X-Cache = "PASS:" + resp.http.X-Pass-Reason; - } else { - set resp.http.X-Cache = "PASS"; + unset req.http.X-Varnish-CS-Entry; + + # Nie oznaczamy lokalnych błędów jako odpowiedzi backendu + if (!resp.http.X-Varnish-Local-Error) { + + # HIT - również stale/grace dostarczony z cache + if (req.http.X-Varnish-CS-Mode == "hit") { + set req.http.X-Varnish-CS-Entry = "varnish; hit"; + + # TTL jako integer, również ujemny + set req.http.X-Varnish-CS-Entry = + req.http.X-Varnish-CS-Entry + + "; ttl=" + + std.real2integer(std.real(obj.ttl, 0.0), 0); + + # MISS / STALE - pobranie z backendu + } else if (req.http.X-Varnish-CS-Mode == "miss" || + req.http.X-Varnish-CS-Mode == "stale") { + + set req.http.X-Varnish-CS-Entry = + "varnish; fwd=" + req.http.X-Varnish-CS-Mode; + + if (!obj.uncacheable && obj.ttl > 0s) { + set req.http.X-Varnish-CS-Entry = + req.http.X-Varnish-CS-Entry + "; stored"; + + set req.http.X-Varnish-CS-Entry = + req.http.X-Varnish-CS-Entry + + "; ttl=" + + std.real2integer(std.real(obj.ttl, 0.0), 0); + } + + # PASS / metoda wymagająca forwardowania + } else if (req.http.X-Varnish-CS-Mode == "bypass" || + req.http.X-Varnish-CS-Mode == "method") { + + set req.http.X-Varnish-CS-Entry = + "varnish; fwd=" + req.http.X-Varnish-CS-Mode; } - unset resp.http.X-Pass-Reason; - unset resp.http.Age; - } else if (obj.hits > 0) { - set resp.http.X-Cache = "HIT"; - } else { - set resp.http.X-Cache = "MISS"; + + # Opcjonalny detail z backend_response + if (req.http.X-Varnish-CS-Entry && + resp.http.X-Varnish-CS-Detail) { + set req.http.X-Varnish-CS-Entry = + req.http.X-Varnish-CS-Entry + + "; detail=" + resp.http.X-Varnish-CS-Detail; + } + + # RFC 9211: zachowaj wpisy poprzednich cache + if (req.http.X-Varnish-CS-Entry) { + if (resp.http.Cache-Status) { + set resp.http.Cache-Status = + resp.http.Cache-Status + ", " + + req.http.X-Varnish-CS-Entry; + } else { + set resp.http.Cache-Status = + req.http.X-Varnish-CS-Entry; + } + } + } + + # Czyszczenie wewnętrznych nagłówków + unset resp.http.X-Varnish-CS-Detail; + unset resp.http.X-Varnish-Local-Error; + unset resp.http.X-Pass-Reason; + unset resp.http.X-Cache; + + # Age zachowujemy dla odpowiedzi z cache + if (obj.uncacheable || obj.hits == 0) { unset resp.http.Age; } @@ -254,11 +385,14 @@ sub vcl_deliver { unset resp.http.Server; } +# ===== SYNTH ===== sub vcl_synth { - set resp.http.X-Cache = "SYNTH"; + # RFC 9211: bez Cache-Status dla lokalnych odpowiedzi + unset resp.http.Cache-Status; + unset resp.http.X-Cache; } -# ===== PURGE HANDLER ===== +# ===== PURGE ===== sub vcl_purge { return (synth(200, "Purged")); }