vcl 4.1; import std; import vsthrottle; # Cache-Status zgodny z RFC 9211. Wymaga vmod_vsthrottle. backend default { .host = "plik"; .port = "8080"; .max_connections = 100; .probe = { .url = "/"; .interval = 10s; .timeout = 5s; .window = 5; .threshold = 3; } .connect_timeout = 5s; .first_byte_timeout = 90s; .between_bytes_timeout = 2s; } acl purge { "localhost"; "127.0.0.1"; "::1"; } sub vcl_recv { # Wyczyść nieufne nagłówki diagnostyczne od klienta. unset req.http.X-Cache; unset req.http.X-Cache-Hits; unset req.http.Cache-Status; unset req.http.X-CS-Decision; if (req.http.Host) { set req.http.Host = regsub(req.http.Host, ":[0-9]+$", ""); } unset req.http.Proxy; set req.backend_hint = default; set req.url = std.querysort(req.url); set req.url = regsub(req.url, "\?$", ""); if (req.restarts == 0) { if (req.http.X-Forwarded-For) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } if (vsthrottle.is_denied(client.identity, 100, 10s, 10s)) { return (synth(429, "Too Many Requests")); } if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, "Not allowed.")); } return (purge); } if (req.method == "BAN") { if (!client.ip ~ purge) { return (synth(405, "Not allowed.")); } # Lokalny BAN zachowuje pierwotną semantykę wyrażenia regularnego. ban("req.http.host == " + req.http.host + " && req.url ~ " + req.url); return (synth(200, "Banned")); } # OPTIONS nie jest cache'owalne; inne metody niż GET/HEAD też pomijamy. if (req.method != "GET" && req.method != "HEAD") { set req.http.X-CS-Decision = "method"; return (pass); } if (req.http.Authorization) { set req.http.X-CS-Decision = "bypass"; return (pass); } # Klient wymaga ponownej walidacji: bezpiecznie kierujemy do backendu. if (req.http.Cache-Control ~ "(?i)(^|,)[[:space:]]*(no-cache|max-age=0)([[:space:]]*(,|$))" || req.http.Pragma ~ "(?i)no-cache") { set req.http.X-CS-Decision = "request"; return (pass); } # upload / status / api / ajax poza cache. if ( req.url ~ "(?i)^/(upload|api|status|progress|session|login|logout)(/|$|\?)" || req.url ~ "(?i)/(ajax|ahah)/" ) { set req.http.X-CS-Decision = "bypass"; return (pass); } if (req.http.Accept-Encoding) { if (req.url ~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|woff2?|ttf|eot|mp4|mp3|pdf|zip|7z|gz|bz2)(\?.*)?$") { unset req.http.Accept-Encoding; } elseif (req.http.Accept-Encoding ~ "(?i)gzip") { set req.http.Accept-Encoding = "gzip"; } else { unset req.http.Accept-Encoding; } } # Cookies ignorujemy wyłącznie dla zasobów statycznych. if (req.http.Cookie) { if (req.url ~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|js|css|woff2?|ttf|eot|pdf|zip|7z|gz|bz2|mp4|mp3)(\?.*)?$") { unset req.http.Cookie; } else { set req.http.X-CS-Decision = "bypass"; return (pass); } } return (hash); } sub vcl_hash { # Domyślny VCL dodaje req.url i Host. Dodajemy rozróżnienie HTTP/HTTPS. if (req.http.X-Forwarded-Proto) { hash_data(req.http.X-Forwarded-Proto); } } sub vcl_hit { if (obj.ttl >= 0s) { set req.http.X-CS-Decision = "hit"; return (deliver); } if (obj.ttl + obj.grace > 0s) { set req.http.X-CS-Decision = "hit"; return (deliver); } # Odswiezenie po wyczerpaniu grace to fwd=stale, nie uri-miss. set req.http.X-CS-Decision = "stale"; return (miss); } sub vcl_miss { if (!req.http.X-CS-Decision) { set req.http.X-CS-Decision = "miss"; } return (fetch); } sub vcl_pass { if (!req.http.X-CS-Decision) { set req.http.X-CS-Decision = "bypass"; } return (fetch); } sub vcl_backend_fetch { # Metadane pomocnicze nie trafiają do aplikacji. unset bereq.http.X-CS-Decision; } sub vcl_backend_response { # Dla PASS nie zmieniaj polityki cache ani cookies aplikacji. if (bereq.uncacheable) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } # Odpowiedzi 5xx i tresci jawnie prywatne nie moga trafic do cache. if (beresp.status >= 500 && beresp.status <= 599) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } if (beresp.http.Cache-Control ~ "(?i)(no-store|private|no-cache)" || beresp.http.Surrogate-Control ~ "(?i)no-store" || beresp.http.Vary ~ "^[[:space:]]*\*[[:space:]]*$") { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } # Odpowiedź personalizowana z Set-Cookie nie powinna być współdzielona. if (bereq.url !~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|js|css|woff2?|ttf|eot|pdf|zip|7z|gz|bz2|mp4|mp3)(\?.*)?$" && beresp.http.Set-Cookie) { set beresp.uncacheable = true; set beresp.ttl = 0s; return (deliver); } # Krótki cache dla 404, umiarkowany dla przekierowań. if (beresp.status == 404) { set beresp.ttl = 5s; } elseif (beresp.status == 301 || beresp.status == 302) { set beresp.ttl = 10m; } # Wariant: statyki bez jawnej polityki pochodzenia dostają 7 dni. if (beresp.status == 200 && bereq.url ~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|js|css|woff2?|ttf|eot|pdf|zip|7z|gz|bz2|mp4|mp3)(\?.*)?$" && !beresp.http.Cache-Control && !beresp.http.Expires) { set beresp.ttl = 7d; set beresp.http.Cache-Control = "public, max-age=604800, immutable"; } # Gdy Varnish nie ma dodatniego TTL, odczytaj nagłówki cache. if (beresp.ttl <= 0s) { 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 ); } elseif (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 ); } } if (beresp.ttl <= 0s) { set beresp.ttl = 0s; set beresp.uncacheable = true; return (deliver); } # Set-Cookie usuwamy tylko przy odpowiedzi 200 z publicznym plikiem. if (beresp.status == 200 && bereq.url ~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|js|css|woff2?|ttf|eot|pdf|zip|7z|gz|bz2|mp4|mp3)(\?.*)?$") { unset beresp.http.Set-Cookie; } # Kompresuj tylko nieskompresowane typy tekstowe. if (!beresp.http.Content-Encoding && beresp.http.Content-Type ~ "(?i)^(text/|application/(javascript|json|xml))") { set beresp.do_gzip = true; } else { set beresp.do_gzip = false; } if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } set beresp.grace = 10m; if (beresp.ttl > 1h) { set beresp.keep = 1h; } else { set beresp.keep = beresp.ttl; } if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { set beresp.do_stream = true; } return (deliver); } sub vcl_deliver { # Polityka przeglądarki: nie nadpisuj private/no-store backendu na public. if (req.url ~ "(?i)\.(jpg|jpeg|png|gif|webp|ico|svg|js|css|woff2?|ttf|eot|pdf|zip|7z|gz|bz2|mp4|mp3)(\?.*)?$" && resp.status == 200 && !obj.uncacheable) { if (!resp.http.Cache-Control) { set resp.http.Cache-Control = "public, max-age=604800, immutable"; } } elseif (!resp.http.Cache-Control || resp.http.Cache-Control !~ "(?i)(no-store|private)") { set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0"; } # Odpowiedz z vcl_backend_error to lokalny synthetic response. # RFC 9211 zaleca, aby nie oznaczac jej jako MISS. if (resp.http.X-CS-Local-Error) { unset resp.http.Cache-Status; } else { # RFC 9211: status dotyczy obsługi bieżącego żądania (nie obiektu w cache). # Dodajemy własny wpis NA KOŃCU, zachowując wpisy wcześniejszych cache. if (req.http.X-CS-Decision == "hit") { # obj.ttl ma format np. 123.456; RFC wymaga całkowitych sekund. # Wariant działa również ze starszym std (Varnish 6.0/6.1). set resp.http.X-CS-TTL = obj.ttl; set resp.http.X-CS-TTL = regsub(resp.http.X-CS-TTL, "[.][0-9]+$", ""); set resp.http.X-CS-Entry = "varnish; hit; ttl=" + resp.http.X-CS-TTL; } elseif (req.http.X-CS-Decision == "method") { set resp.http.X-CS-Entry = "varnish; fwd=method"; } elseif (req.http.X-CS-Decision == "request") { set resp.http.X-CS-Entry = "varnish; fwd=request"; } elseif (req.http.X-CS-Decision == "bypass") { set resp.http.X-CS-Entry = "varnish; fwd=bypass"; } elseif (req.http.X-CS-Decision == "miss" || req.http.X-CS-Decision == "stale") { if (!obj.uncacheable && obj.ttl > 0s) { set resp.http.X-CS-Entry = "varnish; fwd=" + regsub(req.http.X-CS-Decision, "^miss$", "uri-miss") + "; stored"; } else { set resp.http.X-CS-Entry = "varnish; fwd=" + regsub(req.http.X-CS-Decision, "^miss$", "uri-miss") + "; stored=?0"; } } else { # Zabezpieczenie na nietypowe ścieżki VCL. if (obj.uncacheable) { set resp.http.X-CS-Entry = "varnish; fwd=bypass"; } elseif (obj.hits > 0) { set resp.http.X-CS-TTL = obj.ttl; set resp.http.X-CS-TTL = regsub(resp.http.X-CS-TTL, "[.][0-9]+$", ""); set resp.http.X-CS-Entry = "varnish; hit; ttl=" + resp.http.X-CS-TTL; } else { set resp.http.X-CS-Entry = "varnish; fwd=uri-miss"; } } if (resp.http.Cache-Status) { set resp.http.Cache-Status = resp.http.Cache-Status + ", " + resp.http.X-CS-Entry; } else { set resp.http.Cache-Status = resp.http.X-CS-Entry; } } unset resp.http.X-CS-Entry; unset resp.http.X-CS-TTL; unset resp.http.X-CS-Local-Error; unset resp.http.X-Cache; unset resp.http.X-Cache-Hits; unset resp.http.X-Url; unset resp.http.X-Host; unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; unset resp.http.Content-Disposition; set resp.http.X-Frame-Options = "SAMEORIGIN"; } sub vcl_backend_error { # Zachowaj standardowa tresc bledu z built-in VCL, bez cache'owania 503. set beresp.ttl = 0s; set beresp.http.X-CS-Local-Error = "1"; } sub vcl_synth { # Odpowiedzi syntetyczne (np. 429) nie raportują stanu cache. unset resp.http.Cache-Status; unset resp.http.X-Cache; unset resp.http.X-Cache-Hits; unset resp.http.X-Varnish; set resp.http.Cache-Control = "private, no-store"; if (resp.status == 429) { set resp.http.Content-Type = "text/plain; charset=utf-8"; return (deliver); } # Dla PURGE/BAN i innych SYNTH zachowaj tresc built-in VCL. }