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 { # RATE LIMIT: 200 żądań / 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); } # 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); } # Żądania z Authorization nie są buforowane if (req.http.Authorization) { return (pass); } # ---- Normalizacja Accept-Encoding (kolejność: 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 ---- # 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); } 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) ===== 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 { # Zakaz cache – respektujemy 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"; return (deliver); } # NIE cache'uj redirectów do loginu (HTML) z backendu if (beresp.status >= 300 && beresp.status < 400) { set beresp.uncacheable = true; set beresp.ttl = 0s; set beresp.http.X-Pass-Reason = "redirect"; return (deliver); } # Nie cache'uj statyków, jeśli status ≠ 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; 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") { set beresp.uncacheable = true; set beresp.ttl = 0s; 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"; } } # ---- 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) { 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; } } # Immutable => dłuższe grace/keep if (beresp.http.Cache-Control ~ "(?i)immutable") { set beresp.grace = 1h; 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)") { 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; } } # (Opcjonalnie) Serwuj „stale” przy błędach backendu, jeśli jest obiekt w grace sub vcl_backend_error { return (deliver); } # ===== DELIVER ===== 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 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"; unset resp.http.Age; } unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; } sub vcl_synth { set resp.http.X-Cache = "SYNTH"; } # ===== PURGE HANDLER ===== sub vcl_purge { return (synth(200, "Purged")); }