This commit is contained in:
Mateusz Gruszczyński committed 2026-10-26 16:40:23 +01:00
1 parent 191c2bf004
commit 33c3ec0a0b
1 file changed
+252 -118
+252 -118
View File
@@ -1,9 +1,10 @@
vcl 4.1; vcl 4.1;
import vsthrottle; import vsthrottle;
import std; import std;
# ===== Backend ===== # ===== BACKEND =====
backend app { backend app {
.host = "app"; .host = "app";
.port = "${APP_PORT}"; .port = "${APP_PORT}";
@@ -17,32 +18,49 @@ acl purge {
# ===== RECV ===== # ===== RECV =====
sub vcl_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)) { if (vsthrottle.is_denied(client.identity, 200, 10s, 60s)) {
return (synth(429, "Too Many Requests")); return (synth(429, "Too Many Requests"));
} }
# PURGE tylko lokalnie # PURGE tylko lokalnie
if (req.method == "PURGE") { if (req.method == "PURGE") {
if (!client.ip ~ purge) { return (synth(405, "Not allowed")); } if (!client.ip ~ purge) {
return (synth(405, "Not allowed"));
}
return (purge); return (purge);
} }
# omijamy cache dla healthchecków / wewnętrznych nagłówków if (!req.http.X-Forwarded-Proto) {
if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); } set req.http.X-Forwarded-Proto = "https";
}
# Specjalna obsługa WebSocket i socket.io # Healthcheck / internal
if (req.http.Upgrade ~ "(?i)websocket" || req.url ~ "^/socket.io/") { 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); return (pipe);
} }
# metody inne niż GET/HEAD bez cache # Metody inne niż GET/HEAD
if (req.method != "GET" && req.method != "HEAD") { return (pass); } if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Żądania z Authorization nie są buforowane # Authorization
if (req.http.Authorization) { return (pass); } 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) {
if (req.http.Accept-Encoding ~ "zstd") { if (req.http.Accept-Encoding ~ "zstd") {
set 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 ---- # Statyczne zasoby
# if (req.url ~ "\.(png|jpe?g|gif|bmp)$") { if (req.url ~ "^/static/" ||
# if (req.http.Accept ~ "image/webp") { req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") {
# 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.Cookie;
unset req.http.Authorization;
return (hash); 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); return (hash);
} }
# ===== PIPE (WebSocket passthrough) ===== # ===== PIPE =====
sub vcl_pipe { sub vcl_pipe {
if (req.http.Upgrade) { if (req.http.Upgrade) {
set bereq.http.Upgrade = req.http.Upgrade; set bereq.http.Upgrade = req.http.Upgrade;
@@ -110,142 +94,289 @@ sub vcl_pipe {
# ===== HASH ===== # ===== HASH =====
sub vcl_hash { sub vcl_hash {
hash_data(req.url); 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.host) {
if (req.http.Cookie) { hash_data(req.http.Cookie); } hash_data(req.http.host);
} else {
hash_data(server.ip);
}
# Accept-Encoding: już znormalizowany do zstd/br/gzip/identity if (req.http.Cookie) {
if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); } hash_data(req.http.Cookie);
}
# (Opcjonalnie) sygnał obrazów z negocjacją po Accept if (req.http.Accept-Encoding) {
if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); } 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 { 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") { if (beresp.http.Cache-Control ~ "(?i)no-store|private") {
set beresp.uncacheable = true; set beresp.uncacheable = true;
set beresp.ttl = 0s; set beresp.ttl = 0s;
set beresp.http.X-Pass-Reason = "no-store"; set beresp.http.X-Varnish-CS-Detail = "uncacheable";
return (deliver); return (deliver);
} }
# NIE cache'uj redirectów do loginu (HTML) z backendu # Redirecty bez cache
if (beresp.status >= 300 && beresp.status < 400) { if (beresp.status >= 300 && beresp.status < 400) {
set beresp.uncacheable = true; set beresp.uncacheable = true;
set beresp.ttl = 0s; set beresp.ttl = 0s;
set beresp.http.X-Pass-Reason = "redirect"; set beresp.http.X-Varnish-CS-Detail = "redirect";
return (deliver); return (deliver);
} }
# Nie cache'uj statyków, jeśli status ≠ 200 # Statyczne: cache tylko HTTP 200
if (bereq.url ~ "^/static/" || 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) { if (beresp.status != 200) {
set beresp.uncacheable = true; set beresp.uncacheable = true;
set beresp.ttl = 0s; set beresp.ttl = 0s;
set beresp.http.X-Varnish-CS-Detail = "invalid-static-status";
return (deliver); return (deliver);
} }
} }
# Jeśli pod .js przychodzi text/html — też nie cache'uj (to zwykle redirect/login) # HTML zamiast JS/CSS - nie cache'uj
if (bereq.url ~ "\.js(\?.*)?$" && beresp.http.Content-Type ~ "(?i)text/html") { if (bereq.url ~ "\.(js|css)(\?.*)?$" &&
beresp.http.Content-Type ~ "(?i)text/html") {
set beresp.uncacheable = true; set beresp.uncacheable = true;
set beresp.ttl = 0s; set beresp.ttl = 0s;
set beresp.http.X-Varnish-CS-Detail = "html-instead-of-static";
return (deliver); return (deliver);
} }
# Wymuś poprawny Content-Type dla .js/.css, gdy backend zwróci HTML # Content-Type przy brakującym nagłówku
if (bereq.url ~ "\.js(\?.*)?$") { if (bereq.url ~ "\.js(\?.*)?$" &&
if (!beresp.http.Content-Type || beresp.http.Content-Type ~ "(?i)text/html") { !beresp.http.Content-Type) {
set beresp.http.Content-Type = "application/javascript; charset=utf-8"; 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 ~ "\.css(\?.*)?$" &&
if (bereq.url ~ "^/static/" || bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$") { !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; 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) { if (beresp.http.Vary) {
set beresp.http.Vary = regsuball(beresp.http.Vary, "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", "\1"); set beresp.http.Vary = regsuball(
set beresp.http.Vary = regsuball(beresp.http.Vary, ",[[:space:]]*,", ","); beresp.http.Vary,
set beresp.http.Vary = regsub(beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", ""); "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)",
set beresp.http.Vary = regsub(beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", ""); "\1"
if (beresp.http.Vary ~ "^[[:space:]]*$") { unset beresp.http.Vary; } );
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)")) { if (!(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) {
set beresp.ttl = 24h; 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.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]+)") { 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]+)") { } 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) { } else if (beresp.http.Expires) {
set beresp.ttl = std.time(beresp.http.Expires, now) - now; 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 { } 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") { if (beresp.http.Cache-Control ~ "(?i)immutable") {
set beresp.grace = 1h; set beresp.grace = 1h;
set beresp.keep = 24h; set beresp.keep = 24h;
} }
# Kompresja po stronie Varnisha wyłącznie dla klientów akceptujących gzip # ===== GZIP =====
# i tylko jeśli backend nie dostarczył już Content-Encoding. if (!beresp.http.Content-Encoding &&
if (!beresp.http.Content-Encoding && bereq.http.Accept-Encoding ~ "gzip") { bereq.http.Accept-Encoding ~ "gzip") {
# Kompresujemy tylko „tekstowe” typy; wykluczamy WASM
if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml)") { if (beresp.http.Content-Type ~
"(?i)text/|application/(javascript|json|xml)") {
set beresp.do_gzip = true; set beresp.do_gzip = true;
} }
} }
# Duże odpowiedzi streamujemy # ===== STREAMING =====
if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { if (beresp.http.Content-Length &&
std.integer(beresp.http.Content-Length, 0) > 1048576) {
set beresp.do_stream = true; set beresp.do_stream = true;
} }
} }
# (Opcjonalnie) Serwuj „stale” przy błędach backendu, jeśli jest obiekt w grace # ===== BACKEND ERROR =====
sub vcl_backend_error { sub vcl_backend_error {
# Odpowiedź wygenerowana lokalnie - bez Cache-Status
set beresp.http.X-Varnish-Local-Error = "1";
return (deliver); return (deliver);
} }
# ===== DELIVER ===== # ===== DELIVER: RFC 9211 =====
sub vcl_deliver { sub vcl_deliver {
if (obj.uncacheable) { unset req.http.X-Varnish-CS-Entry;
if (req.http.X-Pass-Reason) {
set resp.http.X-Cache = "PASS:" + req.http.X-Pass-Reason; # Nie oznaczamy lokalnych błędów jako odpowiedzi backendu
} else if (resp.http.X-Pass-Reason) { # z backendu if (!resp.http.X-Varnish-Local-Error) {
set resp.http.X-Cache = "PASS:" + resp.http.X-Pass-Reason;
} else { # HIT - również stale/grace dostarczony z cache
set resp.http.X-Cache = "PASS"; 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; # Opcjonalny detail z backend_response
} else if (obj.hits > 0) { if (req.http.X-Varnish-CS-Entry &&
set resp.http.X-Cache = "HIT"; resp.http.X-Varnish-CS-Detail) {
} else { set req.http.X-Varnish-CS-Entry =
set resp.http.X-Cache = "MISS"; 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.Age;
} }
@@ -254,11 +385,14 @@ sub vcl_deliver {
unset resp.http.Server; unset resp.http.Server;
} }
# ===== SYNTH =====
sub vcl_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 { sub vcl_purge {
return (synth(200, "Purged")); return (synth(200, "Purged"));
} }