vcl 4.1; import vsthrottle; import std; # ===== BACKEND ===== backend app { .host = "app"; .port = "${APP_PORT}"; } # ===== ACL ===== acl purge { "127.0.0.1"; "::1"; } # ===== RECV ===== sub vcl_recv { # 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")); } return (purge); } if (!req.http.X-Forwarded-Proto) { set req.http.X-Forwarded-Proto = "https"; } # 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 if (req.method != "GET" && req.method != "HEAD") { return (pass); } # Authorization if (req.http.Authorization) { return (pass); } # Accept-Encoding 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"; } } # Statyczne zasoby if (req.url ~ "^/static/" || req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { unset req.http.Cookie; return (hash); } return (hash); } # ===== PIPE ===== 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); } if (req.http.Cookie) { hash_data(req.http.Cookie); } 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); } # ===== 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 { # 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-Varnish-CS-Detail = "uncacheable"; return (deliver); } # Redirecty bez cache if (beresp.status >= 300 && beresp.status < 400) { set beresp.uncacheable = true; set beresp.ttl = 0s; set beresp.http.X-Varnish-CS-Detail = "redirect"; return (deliver); } # Statyczne: cache tylko HTTP 200 if (bereq.url ~ "^/static/" || 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); } } # 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); } # 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"; } 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; # 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; } } 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; } # ===== 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 ); } 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) { set beresp.ttl = std.time(beresp.http.Expires, now) - now; if (beresp.ttl < 0s) { set beresp.ttl = 0s; } } else { if (beresp.ttl <= 0s) { set beresp.ttl = 60s; } } if (beresp.http.Cache-Control ~ "(?i)immutable") { set beresp.grace = 1h; set beresp.keep = 24h; } # ===== 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; } } # ===== STREAMING ===== if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { set beresp.do_stream = true; } } # ===== BACKEND ERROR ===== sub vcl_backend_error { # Odpowiedź wygenerowana lokalnie - bez Cache-Status set beresp.http.X-Varnish-Local-Error = "1"; return (deliver); } # ===== DELIVER: RFC 9211 ===== sub vcl_deliver { 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; } # 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; } unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; } # ===== SYNTH ===== sub vcl_synth { # RFC 9211: bez Cache-Status dla lokalnych odpowiedzi unset resp.http.Cache-Status; unset resp.http.X-Cache; } # ===== PURGE ===== sub vcl_purge { return (synth(200, "Purged")); }