Last active
October 16, 2017 15:29
-
-
Save timkelty/e252be8bfa5e78a6f38b4942ea19ff20 to your computer and use it in GitHub Desktop.
VCL 4.0 (Load Balanced, SSL termination, ESI, sticky sessions, PURGE, BAN, some specifics for Craft CMS)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
vcl 4.0; | |
import std; | |
import directors; | |
import cookie; | |
import header; | |
backend web01 { | |
.host = "web01.myserver.com"; | |
.port = "80"; | |
.max_connections = 300; | |
.probe = { | |
.request = | |
"HEAD / HTTP/1.1" | |
"Host: varnish.myserver.com" | |
"Connection: close" | |
"User-Agent: Varnish Health Probe"; | |
.interval = 20s; # check the health of each backend every 5 seconds | |
.timeout = 10s; # timing out after 5 seconds. | |
.window = 10; | |
.threshold = 3; | |
} | |
.first_byte_timeout = 300s; # How long to wait before we receive a first byte from our backend? | |
.connect_timeout = 10s; # How long to wait for a backend connection? | |
.between_bytes_timeout = 5s; # How long to wait between bytes received from our backend? | |
} | |
backend web02 { | |
.host = "web02.myserver.com"; | |
.port = "80"; | |
.max_connections = 300; | |
.probe = { | |
.request = | |
"HEAD / HTTP/1.1" | |
"Host: varnish.myserver.com" | |
"Connection: close" | |
"User-Agent: Varnish Health Probe"; | |
.interval = 20s; # check the health of each backend every 5 seconds | |
.timeout = 10s; # timing out after 5 seconds. | |
.window = 10; | |
.threshold = 3; | |
} | |
.first_byte_timeout = 300s; # How long to wait before we receive a first byte from our backend? | |
.connect_timeout = 10s; # How long to wait for a backend connection? | |
.between_bytes_timeout = 5s; # How long to wait between bytes received from our backend? | |
} | |
sub vcl_init { | |
new vdir = directors.hash(); | |
vdir.add_backend(web01, 1.0); | |
vdir.add_backend(web02, 1.0); | |
} | |
acl purge { | |
"127.0.0.1"; | |
"some.other.ip.you.want.to.allow.purging.from"; | |
"localhost"; | |
} | |
sub vcl_recv { | |
# Banning. Use a url regexp in X-Ban-Pattern | |
if (req.method == "BAN" && req.http.X-Ban-Pattern) { | |
if (!client.ip ~ purge) { | |
# Not from an allowed IP? Then die with an error. | |
return (synth(405, "This IP is not allowed to send PURGE requests.")); | |
} | |
ban("req.url ~ " + req.http.X-Ban-Pattern); | |
return(synth(200, "Ban added")); | |
} | |
# Allow purging | |
if (req.method == "PURGE") { | |
if (!client.ip ~ purge) { | |
# Not from an allowed IP? Then die with an error. | |
return (synth(405, "This IP is not allowed to send PURGE requests.")); | |
} | |
# If you got this stage (and didn't error out above), purge the cached result | |
return (purge); | |
} | |
# Load balance based on session | |
if (req.url ~ "(\/cp|p=cp)(.*)" || | |
req.url ~ "^/users.*" || | |
req.url ~ "^/store/cart.*" || | |
req.url ~ "^/store/checkout.*" || | |
req.url ~ "^/store/customer.*" || | |
req.url ~ "/members.*" || | |
req.url ~ "/account.*" || | |
req.url ~ "/actions.*" || | |
req.url ~ "/reviews/new.*" || | |
req.url ~ "token=" || | |
req.method == "POST") { | |
cookie.parse(req.http.cookie); | |
if (cookie.get("CraftSessionId")) { | |
set req.backend_hint = vdir.backend(cookie.get("CraftSessionId")); | |
# For debugging with varnishlog | |
set req.http.X-Backend = req.backend_hint; | |
} | |
cookie.filter_except("CraftSessionId"); | |
return(pass); | |
} else { | |
unset req.http.Cookie; | |
# If we're not using the session to select a backend, we use a random number. | |
# this gives us a better load balancing distribution (theoretically) | |
set req.backend_hint = vdir.backend(std.random(1, 1000)); | |
} | |
# Force https | |
if (req.http.host ~ "(?i)myserver" && | |
req.http.X-Forwarded-Proto !~ "(?i)https" && | |
req.method != "PURGE") { | |
set req.http.x-redir = "https://" + req.http.host + req.url; | |
return (synth(750, "")); | |
} | |
if (req.restarts == 0) { | |
unset req.http.X-Purger; | |
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; | |
} | |
} | |
# Normalize the header, remove the port (in case you're testing this on various TCP ports) | |
set req.http.Host = regsub(req.http.Host, ":[0-9]+", ""); | |
# Normalize the query arguments | |
set req.url = std.querysort(req.url); | |
# Only deal with "normal" types | |
if (req.method != "GET" && | |
req.method != "HEAD" && | |
req.method != "PUT" && | |
req.method != "POST" && | |
req.method != "TRACE" && | |
req.method != "OPTIONS" && | |
req.method != "PATCH" && | |
req.method != "DELETE") { | |
/* Non-RFC2616 or CONNECT which is weird. */ | |
return (pipe); | |
} | |
# Only cache GET or HEAD requests. A fallback for wacky HTTP methods, | |
# IOW, this should never have to exist, but due to people being malicious, it does. | |
if (req.method != "GET" && req.method != "HEAD") { | |
return (pass); | |
} | |
# Some generic URL manipulation, useful for all templates that follow | |
# First remove the Google Analytics added parameters, useless for our backend | |
if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|gclid|cx|ie|cof|siteurl)=") { | |
set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", ""); | |
set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "?"); | |
set req.url = regsub(req.url, "\?&", "?"); | |
set req.url = regsub(req.url, "\?$", ""); | |
} | |
# Strip hash, server doesn't need it. | |
if (req.url ~ "\#") { | |
set req.url = regsub(req.url, "\#.*$", ""); | |
} | |
# Strip a trailing ? if it exists | |
if (req.url ~ "\?$") { | |
set req.url = regsub(req.url, "\?$", ""); | |
} | |
# Remove the pragma header | |
unset req.http.Pragma; | |
# Normalize Accept-Encoding header | |
# straight from the manual: https://www.varnish-cache.org/docs/3.0/tutorial/vary.html | |
if (req.http.Accept-Encoding) { | |
if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") { | |
# No point in compressing these | |
unset req.http.Accept-Encoding; | |
} elsif (req.http.Accept-Encoding ~ "gzip") { | |
set req.http.Accept-Encoding = "gzip"; | |
} elsif (req.http.Accept-Encoding ~ "deflate") { | |
set req.http.Accept-Encoding = "deflate"; | |
} else { | |
# unkown algorithm | |
unset req.http.Accept-Encoding; | |
} | |
} | |
# Normalize Accept-Language | |
if (req.http.Accept-Language) { | |
unset req.http.Accept-Language; | |
} | |
# Large static files are delivered directly to the end-user without | |
# waiting for Varnish to fully read the file first. | |
# Varnish 4 fully supports Streaming, so set do_stream in vcl_backend_response() | |
if (req.url ~ "^[^?]*\.(mp[34]|rar|tar|tgz|gz|wav|zip|bz2|xz|7z|avi|mov|ogm|mpe?g|mk[av])(\?.*)?$") { | |
unset req.http.Cookie; | |
return (hash); | |
} | |
# Remove all cookies for static files | |
# A valid discussion could be held on this line: do you really need to cache static files that don't cause load? Only if you have memory left. | |
# Sure, there's disk I/O, but chances are your OS will already have these files in their buffers (thus memory). | |
# Before you blindly enable this, have a read here: http://mattiasgeniar.be/2012/11/28/stop-caching-static-files/ | |
if (req.url ~ "^[^?]*\.(bmp|bz2|css|doc|eot|flv|gif|gz|ico|jpeg|jpg|js|less|pdf|png|rtf|swf|txt|woff|xml)(\?.*)?$") { | |
unset req.http.Cookie; | |
return (hash); | |
} | |
# Send Surrogate-Capability headers to announce ESI support to backend | |
set req.http.Surrogate-Capability = "key=ESI/1.0"; | |
if (req.http.Authorization) { | |
# Not cacheable by default | |
return (pass); | |
} | |
return (hash); | |
} | |
sub vcl_pipe { | |
# Called upon entering pipe mode. In this mode, the request is passed on to the backend, and any further data from both the client and backend is passed on unaltered until either end closes the connection. Basically, Varnish will degrade into a simple TCP proxy, shuffling bytes back and forth. For a connection in pipe mode, no other VCL subroutine will ever get called after vcl_pipe. | |
# Note that only the first request to the backend will have | |
# X-Forwarded-For set. If you use X-Forwarded-For and want to | |
# have it set for all requests, make sure to have: | |
# set bereq.http.connection = "close"; | |
# here. It is not set by default as it might break some broken web | |
# applications, like IIS with NTLM authentication. | |
#set bereq.http.Connection = "Close"; | |
# Implementing websocket support (https://www.varnish-cache.org/docs/4.0/users-guide/vcl-example-websockets.html) | |
if (req.http.upgrade) { | |
set bereq.http.upgrade = req.http.upgrade; | |
} | |
return (pipe); | |
} | |
sub vcl_pass { | |
# retrn (pass); | |
} | |
# The data on which the hashing will take place | |
sub vcl_hash { | |
hash_data(req.url); | |
if (req.http.host) { | |
hash_data(req.http.host); | |
} else { | |
hash_data(server.ip); | |
} | |
# hash cookies for requests that have them | |
if (req.http.Cookie) { | |
hash_data(req.http.Cookie); | |
} | |
} | |
sub vcl_hit { | |
# Called when a cache lookup is successful. | |
if (obj.ttl >= 0s) { | |
# A pure unadultered hit, deliver it | |
return (deliver); | |
} | |
# We have no fresh fish. Lets look at the stale ones. | |
if (std.healthy(req.backend_hint)) { | |
# Backend is healthy. Limit age to 10s. | |
if (obj.ttl + 10s > 0s) { | |
#set req.http.grace = "normal(limited)"; | |
return (deliver); | |
} else { | |
# No candidate for grace. Fetch a fresh object. | |
return(fetch); | |
} | |
} else { | |
# backend is sick - use full grace | |
if (obj.ttl + obj.grace > 0s) { | |
#set req.http.grace = "full"; | |
return (deliver); | |
} else { | |
# no graced object. | |
return (fetch); | |
} | |
} | |
# fetch & deliver once we get the result | |
return (fetch); # Dead code, keep as a safeguard | |
} | |
sub vcl_miss { | |
return (fetch); | |
} | |
# Handle the HTTP request coming from our backend | |
sub vcl_backend_response { | |
if (beresp.status == 503 && bereq.retries < 5 ) { | |
return(retry); | |
} | |
# It's always good to know which backend we're serving from | |
set beresp.http.X-Backend = beresp.backend.name; | |
# PHP's Pragma - begone | |
unset beresp.http.Pragma; | |
# Pause ESI request and remove Surrogate-Control header | |
if (beresp.http.Surrogate-Control ~ "ESI/1.0") { | |
set beresp.do_esi = true; | |
unset beresp.http.Surrogate-Control; | |
} | |
# Removes Crafts Set-Cookie on everything except admin and other selected url’s | |
if (!(bereq.url ~ "(\/cp|p=cp)(.*)" || | |
bereq.url ~ "^/search(.*)" || | |
bereq.url ~ "^/users.*" || | |
bereq.url ~ "^/account.*" || | |
bereq.url ~ "^/members.*" || | |
bereq.url ~ "^/store/cart.*" || | |
bereq.url ~ "^/store/checkout.*" || | |
bereq.url ~ "^/store/customer.*" || | |
bereq.url ~ "^/reviews/new.*" || | |
bereq.url ~ "token=" || | |
bereq.url ~ "^/actions.*" | |
)) { | |
unset beresp.http.set-cookie; | |
} | |
# Large static files are delivered directly to the end-user without | |
# waiting for Varnish to fully read the file first. | |
# Varnish 4 fully supports Streaming, so use streaming here to avoid locking. | |
if (bereq.url ~ "^[^?]*\.(mp[34]|rar|tar|tgz|gz|wav|zip|bz2|xz|7z|avi|mov|ogm|mpe?g|mk[av])(\?.*)?$") { | |
unset beresp.http.set-cookie; | |
set beresp.do_stream = true; # Check memory usage it'll grow in fetch_chunksize blocks (128k by default) if the backend doesn't send a Content-Length header, so only enable it for big objects | |
set beresp.do_gzip = false; # Don't try to compress it for storage | |
} | |
# Set 2min cache if unset for static files | |
if (beresp.ttl <= 0s || beresp.http.Set-Cookie || beresp.http.Vary == "*") { | |
set beresp.ttl = 120s; | |
set beresp.uncacheable = true; | |
return (deliver); | |
} | |
# Allow stale content, in case the backend goes down. | |
set beresp.grace = 1h; | |
return (deliver); | |
} | |
sub vcl_backend_error { | |
if (beresp.status == 503 && bereq.retries == 5) { | |
synthetic(std.fileread("/etc/varnish/503.html")); | |
return(deliver); | |
} | |
} | |
sub vcl_deliver { | |
if (resp.status == 503) { | |
return(restart); | |
} | |
if (obj.hits > 0) { | |
set resp.http.X-Cache = "HIT"; | |
} else { | |
set resp.http.x-Cache = "MISS"; | |
} | |
if (req.http.X-Purger) { | |
set resp.http.X-Purger = req.http.X-Purger; | |
} | |
# Please note that obj.hits behaviour changed in 4.0, now it counts per objecthead, not per object | |
# and obj.hits may not be reset in some cases where bans are in use. See bug 1492 for details. | |
# So take hits with a grain of salt | |
set resp.http.X-Cache-Hits = obj.hits; | |
# Check if remove cache control was set by backend. | |
if (resp.http.X-Remove-Cache-Control) { | |
# Remove the marker and set cache control headers | |
unset resp.http.X-Remove-Cache-Control; | |
unset resp.http.Expires; | |
set resp.http.Cache-Control = "no-cache"; | |
set resp.http.Pragma = "no-cache"; | |
set resp.http.X-Varnish-Age = resp.http.age; | |
set resp.http.X-Varnish-Server = 1; | |
# By definition we have a fresh object | |
set resp.http.age = "0"; | |
} | |
# Expires headers are messing up our documents | |
if (resp.http.Content-Type ~ "text/html") { | |
unset resp.http.Expires; | |
} | |
# Remove some headers: PHP version | |
unset resp.http.X-Powered-By; | |
# Remove some headers: Apache/nginx version & OS | |
unset resp.http.Server; | |
unset resp.http.X-Varnish; | |
unset resp.http.Via; | |
unset resp.http.Link; | |
return (deliver); | |
} | |
sub vcl_purge { | |
if (req.method == "PURGE") { | |
set req.method = "GET"; | |
set req.http.X-Purge = "Yes"; | |
return(restart); | |
} | |
} | |
sub vcl_synth { | |
if (resp.status == 720) { | |
# We use this special error status 720 to force redirects with 301 (permanent) redirects | |
# To use this, call the following from anywhere in vcl_recv: error 720 "http://host/new.html" | |
set resp.status = 301; | |
set resp.http.Location = resp.reason; | |
return (deliver); | |
} elseif (resp.status == 721) { | |
# And we use error status 721 to force redirects with a 302 (temporary) redirect | |
# To use this, call the following from anywhere in vcl_recv: error 720 "http://host/new.html" | |
set resp.status = 302; | |
set resp.http.Location = resp.reason; | |
return (deliver); | |
} | |
if (resp.status == 750) { | |
set resp.status = 301; | |
set resp.http.Location = req.http.x-redir; | |
return(deliver); | |
} | |
if (resp.status == 503) { | |
synthetic(std.fileread("/etc/varnish/503.html")); | |
return(deliver); | |
} | |
return (deliver); | |
} | |
sub vcl_fini { | |
return (ok); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment